Human-in-the-loop (HITL) workflows

Trust: ★★★☆☆ (0.90) · 0 validations · developer_reference

Published: 2026-05-11 · Source: crawler_authoritative

Tình huống

Guide for developers implementing human approval and oversight in Mastra workflows, covering suspend, resume, and bail patterns for pausing and resuming workflow execution based on human input.

Insight

Human-in-the-loop (HITL) in Mastra workflows allows pausing execution for manual approval, rejection, or gated decisions. The pattern uses suspend() to pause a workflow with a payload providing context to the user. Steps define resumeSchema for expected input data and suspendSchema for the suspension reason. When suspended, access the payload via result.steps[suspendStep[0]].suspendPayload. Use resume() with resumeData to continue, and bail() to stop without error when a human rejects. For multi-turn input, each step has its own resumeSchema/suspendSchema and must be resumed sequentially with separate resume() calls. The createStep function accepts: id, inputSchema, outputSchema, resumeSchema, suspendSchema, and an execute function receiving inputData, resumeData, suspend, and bail parameters. When approved is false in resumeData, call bail() to skip remaining logic; when undefined, call suspend() to wait for human input.

Hành động

  1. Import createWorkflow, createStep from @mastra/core/workflows and z from zod. 2. Define a step with resumeSchema (e.g., { approved: z.boolean() }) and suspendSchema (e.g., { reason: z.string() }). 3. In the execute function, check resumeData.approved: if false, return bail({ reason: '...' }); if undefined, return await suspend({ reason: '...' }). 4. Create the workflow with createWorkflow(), chain steps with .then(), and commit with .commit(). 5. To resume: get the workflow with mastra.getWorkflow(), create a run with workflow.createRun(), start with run.start(), then call run.resume({ step: 'step-id', resumeData: { approved: true } }). 6. Access suspension payload from result.steps[suspendStep[0]].suspendPayload when result.status === 'suspended'.

Kết quả

Workflow pauses at a step when suspend() is called, returning a suspension payload with a reason. When resume() is called with approved data, execution continues from that step. When bail() is called, the workflow completes with ‘success’ status and skips subsequent logic without raising an error. Multi-step workflows require sequential resume calls for each suspended step.

Điều kiện áp dụng

Requires @mastra/core/workflows package. Steps must define resumeSchema and suspendSchema when human input is needed.


Nội dung gốc (Original)

Human-in-the-loop (HITL)

Some workflows need to pause for human input before continuing. When a workflow is suspended, it can return a message explaining why it paused and what’s needed to proceed. The workflow can then either resume or bail based on the input received. This approach works well for manual approvals, rejections, gated decisions, or any step that requires human oversight.

Pausing workflows for human input

Human-in-the-loop input works much like pausing a workflow using suspend(). The key difference is that when human input is required, you can return suspend() with a payload that provides context or guidance to the user on how to continue.

Pausing a workflow with suspend()

import { createWorkflow, createStep } from '@mastra/core/workflows'
import { z } from 'zod'
 
const step1 = createStep({
  id: 'step-1',
  inputSchema: z.object({
    userEmail: z.string(),
  }),
  outputSchema: z.object({
    output: z.string(),
  }),
  resumeSchema: z.object({
    approved: z.boolean(),
  }),
  suspendSchema: z.object({
    reason: z.string(),
  }),
  execute: async ({ inputData, resumeData, suspend }) => {
    const { userEmail } = inputData
    const { approved } = resumeData ?? {}
 
    if (!approved) {
      return await suspend({
        reason: 'Human approval required.',
      })
    }
 
    return {
      output: `Email sent to ${userEmail}`,
    }
  },
})
 
export const testWorkflow = createWorkflow({
  id: 'test-workflow',
  inputSchema: z.object({
    userEmail: z.string(),
  }),
  outputSchema: z.object({
    output: z.string(),
  }),
})
  .then(step1)
  .commit()

Providing user feedback

When a workflow is suspended, you can access the payload returned by suspend() by identifying the suspended step and reading its suspendPayload.

const workflow = mastra.getWorkflow('testWorkflow')
const run = await workflow.createRun()
 
const result = await run.start({
  inputData: {
    userEmail: '[email protected]',
  },
})
 
if (result.status === 'suspended') {
  const suspendStep = result.suspended[0]
  const suspendedPayload = result.steps[suspendStep[0]].suspendPayload
 
  console.log(suspendedPayload)
}

Example output

The data returned by the step can include a reason and help the user understand what’s needed to resume the workflow.

{
  reason: 'Confirm to send email.'
}

Resuming workflows with human input

As with restarting a workflow, use resume() with resumeData to continue a workflow after receiving input from a human. The workflow resumes from the step where it was paused.

Restarting a workflow with resume()

const workflow = mastra.getWorkflow('testWorkflow')
const run = await workflow.createRun()
 
await run.start({
  inputData: {
    userEmail: '[email protected]',
  },
})
 
const handleResume = async () => {
  const result = await run.resume({
    step: 'step-1',
    resumeData: { approved: true },
  })
}

Handling human rejection with bail()

Use bail() to stop workflow execution at a step without triggering an error. This can be useful when a human explicitly rejects an action. The workflow completes with a success status, and any logic after the call to bail() is skipped.

const step1 = createStep({
  execute: async ({ inputData, resumeData, suspend, bail }) => {
    const { userEmail } = inputData
    const { approved } = resumeData ?? {}
 
    if (approved === false) {
      return bail({
        reason: 'User rejected the request.',
      })
    }
 
    if (!approved) {
      return await suspend({
        reason: 'Human approval required.',
      })
    }
 
    return {
      message: `Email sent to ${userEmail}`,
    }
  },
})

Multi-turn human input

For workflows that require input at multiple stages, the suspend pattern remains the same. Each step defines a resumeSchema, and suspendSchema typically with a reason that can be used to provide user feedback.

const step1 = createStep({...});
 
const step2 = createStep({
  id: "step-2",
  inputSchema: z.object({
    message: z.string()
  }),
  outputSchema: z.object({
    output: z.string()
  }),
  resumeSchema: z.object({
    approved: z.boolean()
  }),
  suspendSchema: z.object({
    reason: z.string()
  }),
  execute: async ({ inputData, resumeData, suspend }) => {
    const { message } = inputData;
    const { approved } = resumeData ?? {};
 
    if (!approved) {
      return await suspend({
        reason: "Human approval required."
      });
    }
 
    return {
      output: `${message} - Deleted`
    };
  }
});
 
export const testWorkflow = createWorkflow({
  id: "test-workflow",
  inputSchema: z.object({
    userEmail: z.string()
  }),
  outputSchema: z.object({
    output: z.string()
  })
})
  .then(step1)
  .then(step2)
  .commit();

Each step must be resumed in sequence, with a separate call to resume() for each suspended step. This approach helps manage multi-step approvals with consistent UI feedback and clear input handling at each stage.

const handleResume = async () => {
  const result = await run.resume({
    step: 'step-1',
    resumeData: { approved: true },
  })
}
 
const handleDelete = async () => {
  const result = await run.resume({
    step: 'step-2',
    resumeData: { approved: true },
  })
}

Liên kết

Xem thêm: