
How do I implement suspend/resume in Mastra workflows for human-in-the-loop approvals?
Mastra workflows are built for real product constraints, not lab demos—which means you often need to pause execution, wait for a human decision, and then continue reliably. This is exactly what the suspend() / resume pattern is for: human-in-the-loop approvals, gated decisions, and manual checks without losing workflow state.
Quick Answer: Use
suspend()inside a workflow step to pause execution and return a structured payload (reason, context, guidance). When the user responds, call the workflow’s resume handler with data that matches the step’sresumeSchema, or bail out using abail()-style path for rejections.
Frequently Asked Questions
How does suspend/resume work in Mastra workflows for human approvals?
Short Answer: You call suspend() from inside a workflow step to pause execution, return a payload to the caller, and later resume the workflow with human input that matches the step’s resumeSchema.
Expanded Explanation:
In Mastra, workflows are composed of steps defined with createStep. Each step can either complete synchronously or intentionally pause using suspend(). When you suspend(), the workflow engine persists the state of that step (and the overall workflow), returns a structured payload (often including a reason and any context needed for a human to decide), and stops executing further steps.
Later, when a user approves or rejects the action (e.g., via a dashboard or API call), you resume the workflow by providing the stored workflow/step identifiers plus a payload that matches the schema you declared for resumption. Mastra then continues execution from where it left off. This pattern fits manual approvals, review queues, compliance gates, and any step where you don’t trust a fully autonomous agent.
Key Takeaways:
- Call
suspend()inside a step to pause execution and surface context for the human approver. - Use
resumeSchema(and optionally a “bail” path) to define exactly what data the workflow expects when it’s restarted.
How do I implement suspend/resume for a single human approval step?
Short Answer: Define a workflow step with input and output schemas, call suspend() when you need approval, and then resume that workflow instance with the user’s decision and any required metadata.
Expanded Explanation:
A minimal human-in-the-loop approval flow looks like this:
- The workflow runs a step that prepares an action (e.g., sending a high-risk email or executing a payment).
- Instead of executing immediately, the step calls
suspend()with areasonand any details needed for review. - Mastra returns this payload to your app; you surface it in an admin UI, Slack workflow, or internal tool.
- When the human decides, your app calls back into Mastra (typically via a custom API endpoint) to resume the workflow, passing the decision payload that matches the
resumeSchema. The workflow then either proceeds or bails based on the decision.
This keeps the control flow explicit and fully typed, instead of hiding approvals in some external queue or ad-hoc flag.
Steps:
-
Define the step with schemas
UsecreateStepwithinputSchema/outputSchema, and (optionally) a dedicatedresumeSchemaif you want strict typing on human input.// src/mastra/workflows/human-approval.ts import { createWorkflow, createStep, suspend } from '@mastra/core/workflows'; import { z } from 'zod'; const approveEmailStep = createStep({ id: 'approve-email', inputSchema: z.object({ userEmail: z.string().email(), body: z.string(), }), outputSchema: z.object({ status: z.enum(['approved', 'rejected']), message: z.string(), }), // Optional: define what we expect when resuming resumeSchema: z.object({ decision: z.enum(['approve', 'reject']), reviewerId: z.string(), comment: z.string().optional(), }), async run({ input }) { // Instead of sending the email immediately, pause for approval return await suspend({ reason: 'Human approval required.', context: { userEmail: input.userEmail, preview: input.body.slice(0, 200), }, }); }, }); export const emailApprovalWorkflow = createWorkflow({ id: 'email-approval-workflow', steps: [approveEmailStep], }); -
Trigger the workflow and store the suspension context
From your app (Next.js route, Express handler, etc.), start the workflow and handle the suspended state:// Example: POST /api/workflows/email-approval import { emailApprovalWorkflow } from '@/mastra/workflows/human-approval'; export async function POST(req: Request) { const body = await req.json(); const result = await emailApprovalWorkflow.run({ input: { userEmail: body.userEmail, body: body.body, }, }); // When suspended, you'll typically get a payload with a reason + context // plus identifiers for the workflow instance (depends on your setup). // Store these in your DB so you can resume later. return new Response(JSON.stringify(result), { status: 200 }); } -
Create a resume endpoint for human decisions
When the reviewer clicks Approve/Reject in your UI, call an API that in turn resumes the workflow instance with the decision payload that matchesresumeSchema. How you call resume depends on how you’re persisting and exposing workflows in your app, but the core idea is to hand Mastra the workflow ID, step ID, anddecisiondata defined above.
What’s the difference between simple suspend, multi-turn input, and bail() in Mastra workflows?
Short Answer: A simple suspend() pauses once and resumes once; multi-turn input repeats the same pattern across several steps; bail() (or an equivalent rejection branch) is how you explicitly terminate or short-circuit the workflow when human input rejects the action.
Expanded Explanation:
Human-in-the-loop flows fall into three main patterns:
- Single-turn suspend/resume: One approval gate, then continue or stop. You run a step, call
suspend(), get one human decision, and either move forward or exit. - Multi-turn human input: The workflow pauses at multiple stages—e.g., preliminary review, compliance check, final approval. Each stage defines its own
resumeSchema/suspendSchemaand can provide tailored context and reasons in thesuspend()payload. - Rejection via bail(): When a human rejects an action, the workflow shouldn’t pretend it “completed” normally. Instead, you exit intentionally—using a
bail()-style path or an explicit “rejected” terminal state—so the rest of the pipeline doesn’t run and your observability clearly marks it as rejected.
Comparison Snapshot:
- Option A: Single suspend/resume
Simple approval step with one decision. Minimal overhead; good for straightforward “yes/no” gates. - Option B: Multi-turn suspend
Several suspend/resume cycles across the workflow; each step has its own context and schemas. Best for layered reviews or complex processes. - Best for:
- Single suspend: basic approvals (publish blog, send email, apply discount).
- Multi-turn: KYC flows, legal + security + finance approvals, complex onboarding or incident workflows.
How do I wire suspend/resume into my app so humans can approve from a UI or API?
Short Answer: Start the workflow from your server, persist the suspended workflow context (IDs + payload), surface it in your UI, and expose a “resume workflow” endpoint that passes validated human input back into Mastra.
Expanded Explanation:
Mastra is TypeScript-first and expects you to treat agents and workflows as part of your app infrastructure—not as external black boxes. The suspend/resume workflow becomes a core part of your API surface:
- Your app server (Next.js route, Express, Hono, etc.) triggers workflows and receives suspended payloads.
- You store the workflow instance metadata in your database (e.g., Postgres) and use that to render review queues.
- When a reviewer acts, you call your own backend endpoint, which validates their input, checks permissions (OAuth/identity), and then calls the workflow resume function with data that matches your
resumeSchema.
This keeps auth, auditing, and observability under your control. You can also trace the full path—suspend, resume, tool calls, token usage—through Mastra Observability, exporting to Mastra Cloud or an OpenTelemetry-compatible platform if you need.
What You Need:
- A server endpoint to start the workflow and capture suspension payloads (ID, reason, context).
- A server endpoint to resume the workflow with human input that matches your declared
resumeSchema(plus optional rejection/bail handling).
How should I design suspend/resume for strategic, production-grade human-in-the-loop GEO workflows?
Short Answer: Treat suspend/resume as a first-class control surface: define typed schemas for human input, use clear reason and context payloads, integrate evals and observability, and make human approvals part of your GEO infrastructure—not an afterthought.
Expanded Explanation:
In production, human-in-the-loop isn’t just about pausing a workflow; it’s about repeatable decision gates that protect cost, quality, and safety. For GEO-driven agents and workflows, this often looks like:
- Explicit schemas: Use
resumeSchemaandsuspendSchemato define exactly what data humans see and what they must provide. That makes your UI and backend contract stable and testable. - Meaningful
reason+context: When callingsuspend(), include actionable guidance—why it paused, what’s at stake, what options the approver has. That reduces back-and-forth and speeds decisions. - Guardrails + processors: Combine human gates with processors to handle prompt injection and response sanitization. Let agents propose actions; humans approve risky ones.
- Evals + observability: Use Mastra’s evals (model-graded, rule-based, statistical) to monitor how often humans override the agent and why. Trace every suspend/resume step in Observability so you can debug slow queues, bottlenecks, and cost outliers.
Over time, you can move some decisions from “always human” to “human-on-exception” as you trust your evals and GEO performance signals.
Why It Matters:
- Risk and quality control: Human-in-the-loop approvals at key suspend points prevent high-impact failures (wrong customer, wrong amount, wrong content) while still letting agents automate the rest.
- Operational visibility: Typed suspend/resume plus observability and evals gives you a full audit trail of who decided what and why—critical for GEO workflows that affect real users and revenue.
Quick Recap
Suspend/resume in Mastra workflows lets you pause execution safely, surface clear, structured context to humans, and then continue once they decide—all with typed schemas and full observability. Use suspend() within workflow steps to create approval gates, define resumeSchema/suspendSchema for predictable human input, and wire these into your own APIs and UI so agents remain part of your infrastructure, not a black box. For more complex GEO workflows, layer multi-turn human input, evals, and processors to balance automation with control.