Why Suspend & Resume?
Suspend and resume enables:- Human-in-the-loop: Wait for user approval, input, or decisions
- Async operations: Pause for webhooks, external API callbacks
- Long-running workflows: Break workflows into resumable chunks
- Multi-step forms: Collect user input across multiple sessions
- Approval workflows: Request and wait for approvals before proceeding
Basic Suspend & Resume
Step 1: Define Suspend and Resume Schemas
import { createStep } from '@mastra/core';
import { z } from 'zod';
const approvalStep = createStep({
id: 'request-approval',
inputSchema: z.object({
requestId: z.string(),
amount: z.number(),
}),
outputSchema: z.object({
approved: z.boolean(),
approver: z.string(),
}),
// Data structure when suspending
suspendSchema: z.object({
requestId: z.string(),
reason: z.string().optional(),
}),
// Data structure when resuming
resumeSchema: z.object({
approved: z.boolean(),
approver: z.string(),
}),
execute: async ({ inputData, suspend, resumeData }) => {
// If resuming, use the resume data
if (resumeData) {
return {
approved: resumeData.approved,
approver: resumeData.approver,
};
}
// Otherwise, suspend and wait for approval
return suspend({
requestId: inputData.requestId,
reason: 'Waiting for approval',
});
},
});
Step 2: Execute and Suspend
import { createWorkflow } from '@mastra/core';
const workflow = createWorkflow({
id: 'approval-workflow',
inputSchema: z.object({
requestId: z.string(),
amount: z.number(),
}),
outputSchema: z.object({
approved: z.boolean(),
approver: z.string(),
}),
})
.then(approvalStep)
.commit();
// Start the workflow
const run = await workflow.execute({
inputData: {
requestId: 'req-123',
amount: 1000,
},
});
const result = await run.result();
if (result.status === 'suspended') {
console.log('Workflow suspended, waiting for approval');
console.log('Run ID:', result.runId);
// Save runId to resume later
}
Step 3: Resume the Workflow
// Resume with approval data
const resumedRun = await workflow.resume({
runId: result.runId,
stepId: 'request-approval',
resumeData: {
approved: true,
approver: '[email protected]',
},
});
const finalResult = await resumedRun.result();
if (finalResult.status === 'success') {
console.log('Workflow completed:', finalResult.result);
}
Suspend Options
Thesuspend function accepts optional configuration:
execute: async ({ suspend }) => {
return suspend(
{ requestId: 'req-123' }, // Suspend payload
{
resumeLabel: 'approval-pending', // Label for this suspend point
}
);
}
return suspend(
{ taskId: 'task-123' },
{
resumeLabel: ['approval-needed', 'manager-review'],
}
);
Multiple Suspend Points
A workflow can have multiple steps that suspend:const step1 = createStep({
id: 'collect-basic-info',
inputSchema: z.object({ userId: z.string() }),
outputSchema: z.object({ name: z.string(), email: z.string() }),
suspendSchema: z.object({ userId: z.string() }),
resumeSchema: z.object({ name: z.string(), email: z.string() }),
execute: async ({ resumeData, suspend, inputData }) => {
if (resumeData) return resumeData;
return suspend({ userId: inputData.userId });
},
});
const step2 = createStep({
id: 'collect-preferences',
inputSchema: z.object({ name: z.string(), email: z.string() }),
outputSchema: z.object({ theme: z.string(), notifications: z.boolean() }),
suspendSchema: z.object({ email: z.string() }),
resumeSchema: z.object({ theme: z.string(), notifications: z.boolean() }),
execute: async ({ resumeData, suspend, inputData }) => {
if (resumeData) return resumeData;
return suspend({ email: inputData.email });
},
});
const workflow = createWorkflow({ /* ... */ })
.then(step1)
.then(step2)
.commit();
// First execution - suspends at step1
const run1 = await workflow.execute({ inputData: { userId: 'user-123' } });
// Resume step1
const run2 = await workflow.resume({
runId: run1.runId,
stepId: 'collect-basic-info',
resumeData: { name: 'John Doe', email: '[email protected]' },
});
const result2 = await run2.result();
// Now suspended at step2
// Resume step2
const run3 = await workflow.resume({
runId: run1.runId,
stepId: 'collect-preferences',
resumeData: { theme: 'dark', notifications: true },
});
const finalResult = await run3.result();
// Workflow complete
Conditional Resume
You can conditionally suspend based on runtime data:const conditionalStep = createStep({
id: 'approval-check',
inputSchema: z.object({ amount: z.number() }),
outputSchema: z.object({ approved: z.boolean() }),
suspendSchema: z.object({ amount: z.number() }),
resumeSchema: z.object({ approved: z.boolean() }),
execute: async ({ inputData, suspend, resumeData }) => {
if (resumeData) {
return resumeData;
}
// Auto-approve small amounts
if (inputData.amount < 100) {
return { approved: true };
}
// Require approval for large amounts
return suspend({ amount: inputData.amount });
},
});
Getting Workflow State
Retrieve the current state of a suspended workflow:const runState = await workflow.getRunState(runId);
if (runState.status === 'suspended') {
console.log('Suspended at steps:', runState.suspendedPaths);
console.log('Resume labels:', runState.resumeLabels);
// Check which step is suspended
for (const [stepId, stepResult] of Object.entries(runState.context)) {
if (stepResult.status === 'suspended') {
console.log(`Step ${stepId} is suspended with:`, stepResult.suspendPayload);
}
}
}
Resume by Label
Instead of resuming by step ID, you can resume using labels:const step = createStep({
id: 'approval-step',
inputSchema: z.object({ requestId: z.string() }),
outputSchema: z.object({ status: z.string() }),
suspendSchema: z.object({ requestId: z.string() }),
resumeSchema: z.object({ status: z.string() }),
execute: async ({ suspend, resumeData }) => {
if (resumeData) return resumeData;
return suspend(
{ requestId: 'req-123' },
{ resumeLabel: 'awaiting-manager-approval' }
);
},
});
// Resume using the label
const resumed = await workflow.resume({
runId: run.runId,
resumeLabel: 'awaiting-manager-approval',
resumeData: { status: 'approved' },
});
Accessing Workflow Data in Suspended State
When a workflow is suspended, you can access:const runState = await workflow.getRunState(runId);
// Initial workflow input
const initialInput = runState.context.input;
// Results from completed steps
const step1Result = runState.context['step-1'];
if (step1Result?.status === 'success') {
console.log('Step 1 output:', step1Result.output);
}
// Suspend payload from suspended step
const suspendedStep = runState.context['approval-step'];
if (suspendedStep?.status === 'suspended') {
console.log('Suspend data:', suspendedStep.suspendPayload);
}
Bail Out of Workflow
You can exit a workflow early with a result usingbail:
const step = createStep({
id: 'check-cache',
inputSchema: z.object({ key: z.string() }),
outputSchema: z.object({ value: z.string() }),
execute: async ({ inputData, bail }) => {
const cached = await checkCache(inputData.key);
if (cached) {
// Exit workflow early with cached result
return bail({ value: cached });
}
// Continue with normal flow
return { value: await fetchFreshData(inputData.key) };
},
});
Practical Example: Multi-Step Approval Workflow
import { createWorkflow, createStep } from '@mastra/core';
import { z } from 'zod';
const createRequestStep = createStep({
id: 'create-request',
inputSchema: z.object({ userId: z.string(), amount: z.number() }),
outputSchema: z.object({ requestId: z.string(), amount: z.number() }),
execute: async ({ inputData }) => {
const requestId = `req-${Date.now()}`;
// Save request to database
return { requestId, amount: inputData.amount };
},
});
const managerApprovalStep = createStep({
id: 'manager-approval',
inputSchema: z.object({ requestId: z.string(), amount: z.number() }),
outputSchema: z.object({ managerApproved: z.boolean(), comments: z.string() }),
suspendSchema: z.object({ requestId: z.string() }),
resumeSchema: z.object({ approved: z.boolean(), comments: z.string() }),
execute: async ({ inputData, suspend, resumeData }) => {
if (resumeData) {
return { managerApproved: resumeData.approved, comments: resumeData.comments };
}
// Suspend and wait for manager
return suspend(
{ requestId: inputData.requestId },
{ resumeLabel: 'manager-approval-pending' }
);
},
});
const financeApprovalStep = createStep({
id: 'finance-approval',
inputSchema: z.object({ managerApproved: z.boolean(), comments: z.string() }),
outputSchema: z.object({ financeApproved: z.boolean() }),
suspendSchema: z.object({ managerComments: z.string() }),
resumeSchema: z.object({ approved: z.boolean() }),
execute: async ({ inputData, suspend, resumeData, bail }) => {
// Bail if manager rejected
if (!inputData.managerApproved) {
return bail({ financeApproved: false });
}
if (resumeData) {
return { financeApproved: resumeData.approved };
}
// Suspend for finance approval
return suspend(
{ managerComments: inputData.comments },
{ resumeLabel: 'finance-approval-pending' }
);
},
});
const approvalWorkflow = createWorkflow({
id: 'expense-approval',
inputSchema: z.object({ userId: z.string(), amount: z.number() }),
outputSchema: z.object({
managerApproved: z.boolean(),
financeApproved: z.boolean(),
}),
})
.then(createRequestStep)
.then(managerApprovalStep)
.then(financeApprovalStep)
.commit();
// Usage
const run = await approvalWorkflow.execute({
inputData: { userId: 'user-123', amount: 5000 },
});
// Later: manager approves
await approvalWorkflow.resume({
runId: run.runId,
resumeLabel: 'manager-approval-pending',
resumeData: { approved: true, comments: 'Looks good' },
});
// Later: finance approves
await approvalWorkflow.resume({
runId: run.runId,
resumeLabel: 'finance-approval-pending',
resumeData: { approved: true },
});
Next Steps
Control Flow
Learn about branching and parallel execution
Creating Workflows
Workflow configuration and options