Chapter 9 · Suspend & Resume: Human-in-the-Loop Collaboration
Chapter goals: Master the workflow suspend and resume mechanism, implement human-approval-waits-then-continues human-in-the-loop flows.
9.1 Why Suspend Is Needed
Many critical actions in real business cannot execute fully automatically:
- Transfers require human复核 before completion;
- Mass emails need manager approval before sending;
- Large refunds require风控 sign-off.
The common thread: the flow must pause and wait for external input before continuing — sometimes minutes, sometimes days. Mastra's Suspend/Resume mechanism builds the "pause point" directly into the Workflow: on suspend, execution state persists to Storage; on resume, execution continues precisely from the breakpoint.
9.2 Triggering Suspend Inside a Step
The suspend() in the execute context can interrupt the current step at any time:
// src/mastra/workflows/refund-workflow.ts
import { createStep, createWorkflow } from '@mastra/core/workflows';
import { z } from 'zod';
const reviewStep = createStep({
id: 'review',
inputSchema: z.object({ orderId: z.string(), amount: z.number() }),
outputSchema: z.object({
approved: z.boolean(),
reviewer: z.string(),
orderId: z.string(),
amount: z.number(),
}),
execute: async ({ inputData, suspend }) => {
// Large orders require human approval → suspend the flow
if (inputData.amount > 1000) {
// Data carried by suspend is shown to the approver
await suspend({
message: `Refund of ${inputData.amount} CNY for order ${inputData.orderId} awaits approval`,
});
// Code after suspend will not execute
}
// Small orders auto-approve (only reached when not suspended)
return {
approved: true,
reviewer: 'auto',
orderId: inputData.orderId,
amount: inputData.amount,
};
},
});suspend is a soft interrupt
suspend() interrupts by throwing a specific control-flow signal; do not write "fallback logic" after it — that code only executes when the step is not suspended.
9.3 resume: Resuming from the Breakpoint
// Trigger the workflow → runs until the review step suspends
const run = await mastra.getWorkflow('refund').createRun();
await run.start({ inputData: { orderId: 'A1024', amount: 2500 } });
console.log(run.status); // 'suspended'
// ...a few hours later, the approver clicks approve in the management UI...
// Resume with the same run: inject the approval result as data
const result = await run.resume({
step: 'review',
resumeData: { approved: true, reviewer: 'Manager Zhang' },
});// After resume, the step re-executes execute: distinguish the two entries via resumeData
execute: async ({ inputData, resumeData, suspend }) => {
if (inputData.amount > 1000) {
if (!resumeData) {
// First entry: no approval data → suspend and wait
await suspend({ message: 'Waiting for approval' });
}
// Second entry: resumeData carries the approval result
return {
approved: resumeData!.approved,
reviewer: resumeData!.reviewer,
orderId: inputData.orderId,
amount: inputData.amount,
};
}
return { approved: true, reviewer: 'auto', orderId: inputData.orderId, amount: inputData.amount };
}resumeData is the payload injected on resume, passed separately from inputData, keeping "original input" and "human supplement" from contaminating each other.
9.4 Persistence: Why Suspended State Survives Cross-Process
The suspended run state (completed steps, variable snapshots, suspend payload) is persisted by Storage:
// src/mastra/index.ts —— production config for persistent storage
import { Mastra } from '@mastra/core';
import { LibSQLStore } from '@mastra/libsql';
export const mastra = new Mastra({
storage: new LibSQLStore({ url: process.env.LIBSQL_URL! }),
// With storage, run.resume() remains available after service restart
});This is exactly the capability Mastra officially emphasizes: "pause indefinitely and resume where you left off" — a week-long approval drag is fine; the state sits in the database.
9.5 Chapter Summary
- Suspend/Resume lets flows pause at any step waiting for external input; the standard mechanism for human-in-the-loop;
await suspend(payload)interrupts execution and carries descriptive data;run.resume({ step, resumeData })injects results to continue;- Suspended state is persisted via Storage, surviving任意 duration and service restarts;
- execute may be entered multiple times (before suspend + after resume); use the presence of
resumeDatato distinguish branches.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. What does suspend() actually do inside a workflow step?
2. How does resumeData differ from inputData?
3. What enables a suspended workflow to survive a service restart?
4. In a step that can both suspend and return normally, how do you distinguish the two entry paths?
🛠️ Hands-On Exercises
- Build a refund workflow: small amounts auto-approve, large amounts suspend for human approval; resume with test data and verify the full path.
- Add a second suspend point after approval (e.g., "notify the user"); verify the workflow can suspend at multiple points.
- Restart the dev server while a workflow is suspended; verify that run.resume() still works afterward.