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
- Import
createWorkflow,createStepfrom@mastra/core/workflowsandzfromzod. 2. Define a step with resumeSchema (e.g.,{ approved: z.boolean() }) and suspendSchema (e.g.,{ reason: z.string() }). 3. In the execute function, checkresumeData.approved: if false, returnbail({ reason: '...' }); if undefined, returnawait suspend({ reason: '...' }). 4. Create the workflow withcreateWorkflow(), chain steps with.then(), and commit with.commit(). 5. To resume: get the workflow withmastra.getWorkflow(), create a run withworkflow.createRun(), start withrun.start(), then callrun.resume({ step: 'step-id', resumeData: { approved: true } }). 6. Access suspension payload fromresult.steps[suspendStep[0]].suspendPayloadwhenresult.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.

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.

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 },
})
}Related
Liên kết
- Nền tảng: Dev Framework · Mastra
- Nguồn: https://mastra.ai/docs/workflows/human-in-the-loop
Xem thêm: