Skip to content

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:

typescript
// 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 ​

typescript
// 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' },
});
typescript
// 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:

typescript
// 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 resumeData to 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 ​

  1. Build a refund workflow: small amounts auto-approve, large amounts suspend for human approval; resume with test data and verify the full path.
  2. Add a second suspend point after approval (e.g., "notify the user"); verify the workflow can suspend at multiple points.
  3. Restart the dev server while a workflow is suspended; verify that run.resume() still works afterward.

Next: Chapter 10 · Memory: Conversation History