M
Mastra•3w ago
Mehul

suspend will repeat waitForEvenr

Basically I have a step in my workflow which needs to wait before it is given permission to execute however this step also has hitl suspend. The issue I face is I want it to waitForEvent only once before it executes the step and then go into execution and hitl until it can go to next step. I'm having it hard wrapping my head around the best pattern to approach this. My root problem is I'm building a workflow which follows a user journey into phases. Once a phase ends the user enters the next phase. I could in theory split this into separate workflows however I want a clear vision of a users journey as seen in mastra studio do let me know if there is an alternative?
4 Replies
Mastra Triager
Mastra Triager•3w ago
šŸ“ Created GitHub issue: https://github.com/mastra-ai/mastra/issues/10419 šŸ” If you're experiencing an error, please provide a minimal reproducible example whenever possible to help us resolve it quickly. šŸ™ Thank you for helping us improve Mastra!
Grayson
Grayson•2w ago
Hey Mehul! Sounds like a cool project.
Here's what I'd recommend - keep the waiting and the executing separate:
import { createWorkflow, createStep } from "@mastra/core/workflows";
import { z } from "zod";

// Your step that needs human approval during execution
const phaseStep = createStep({
id: "execute-phase",
inputSchema: z.object({
userId: z.string(),
phaseData: z.any(),
}),
outputSchema: z.object({
result: z.string(),
}),
resumeSchema: z.object({
approved: z.boolean(),
}),
suspendSchema: z.object({
message: z.string(),
}),
execute: async ({ inputData, resumeData, suspend }) => {
// Do your phase work here...

// Need human approval? Pause here
if (!resumeData?.approved) {
return await suspend({
message: "Waiting for approval to move to next phase",
});
}

return { result: "Phase done!" };
},
});

// The full user journey
export const userJourneyWorkflow = createWorkflow({
id: "user-journey",
inputSchema: z.object({
userId: z.string(),
}),
outputSchema: z.object({
complete: z.boolean(),
}),
})
// Phase 1: wait for the gate to open
.waitForEvent("start-phase-1", z.object({ userId: z.string() }))
// Phase 1: do the work (with human pauses as needed)
.then(phaseStep)

// Phase 2: wait for the gate to open
.waitForEvent("start-phase-2", z.object({ userId: z.string() }))
// Phase 2: do the work
.then(phaseStep)

.commit();
import { createWorkflow, createStep } from "@mastra/core/workflows";
import { z } from "zod";

// Your step that needs human approval during execution
const phaseStep = createStep({
id: "execute-phase",
inputSchema: z.object({
userId: z.string(),
phaseData: z.any(),
}),
outputSchema: z.object({
result: z.string(),
}),
resumeSchema: z.object({
approved: z.boolean(),
}),
suspendSchema: z.object({
message: z.string(),
}),
execute: async ({ inputData, resumeData, suspend }) => {
// Do your phase work here...

// Need human approval? Pause here
if (!resumeData?.approved) {
return await suspend({
message: "Waiting for approval to move to next phase",
});
}

return { result: "Phase done!" };
},
});

// The full user journey
export const userJourneyWorkflow = createWorkflow({
id: "user-journey",
inputSchema: z.object({
userId: z.string(),
}),
outputSchema: z.object({
complete: z.boolean(),
}),
})
// Phase 1: wait for the gate to open
.waitForEvent("start-phase-1", z.object({ userId: z.string() }))
// Phase 1: do the work (with human pauses as needed)
.then(phaseStep)

// Phase 2: wait for the gate to open
.waitForEvent("start-phase-2", z.object({ userId: z.string() }))
// Phase 2: do the work
.then(phaseStep)

.commit();
The thing is, waitForEvent and suspend() are different use cases: waitForEvent: "Hey workflow, don't move forward until I get this signal" (workflow-level) suspend(): "Hey step, pause what you're doing right here until someone tells you to continue" (step-level) Trying to combine them in one step gets messy because you're mixing workflow control flow with step execution logic. How You'd Actually Use This
const run = await userJourneyWorkflow.createRunAsync();

// Kick things off
await run.start({ inputData: { userId: "user-123" } });

// When the user finishes their first phase activities, unlock the step
await userJourneyWorkflow.sendEvent({
runId: run.runId,
eventName: "start-phase-1",
payload: { userId: "user-123" },
});

// If the step needs human input, give it what it needs
const result = await run.resume({
step: "execute-phase",
resumeData: { approved: true },
});

// Rinse and repeat for phase 2
await userJourneyWorkflow.sendEvent({
runId: run.runId,
eventName: "start-phase-2",
payload: { userId: "user-123" },
});
const run = await userJourneyWorkflow.createRunAsync();

// Kick things off
await run.start({ inputData: { userId: "user-123" } });

// When the user finishes their first phase activities, unlock the step
await userJourneyWorkflow.sendEvent({
runId: run.runId,
eventName: "start-phase-1",
payload: { userId: "user-123" },
});

// If the step needs human input, give it what it needs
const result = await run.resume({
step: "execute-phase",
resumeData: { approved: true },
});

// Rinse and repeat for phase 2
await userJourneyWorkflow.sendEvent({
runId: run.runId,
eventName: "start-phase-2",
payload: { userId: "user-123" },
});
Let me know if that makes sense and helps you, thanks!
Mehul
MehulOP•2w ago
Dang i thought i had to pass a whole step for wait for event, and wasn't allowed to keep it as simple as passing an object. That keeps things lean! And I'm happy with that šŸ™‚
Abhi Aiyer
Abhi Aiyer•2w ago
This will change in v1! we removed waitForEvent in favor of using suspend/resume. The patttern is the same as Grayson mentioned!

Did you find this page helpful?