Documentation Index
Fetch the complete documentation index at: https://mintlify.com/mastra-ai/mastra/llms.txt
Use this file to discover all available pages before exploring further.
Mastra workflows support suspending execution to wait for external input (like human approval or async events) and resuming from where they left off.
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: 'manager@company.com',
},
});
const finalResult = await resumedRun.result();
if (finalResult.status === 'success') {
console.log('Workflow completed:', finalResult.result);
}
Suspend Options
The suspend function accepts optional configuration:
execute: async ({ suspend }) => {
return suspend(
{ requestId: 'req-123' }, // Suspend payload
{
resumeLabel: 'approval-pending', // Label for this suspend point
}
);
}
You can also use multiple resume labels:
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();
Resume each step individually:
// 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: 'john@example.com' },
});
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 using bail:
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