Skip to main content

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

Build docs developers (and LLMs) love