Skip to main content
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: '[email protected]',
  },
});

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: '[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 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

Build docs developers (and LLMs) love