1"use strict";(self.webpackChunkmastra_docs=self.webpackChunkmastra_docs||[]).push([["11086"],{91987(e,n,t){t.r(n),t.d(n,{metadata:()=>s,default:()=>c,frontMatter:()=>o,contentTitle:()=>i,toc:()=>l,assets:()=>u});var s=JSON.parse('{"id":"workflows/human-in-the-loop","title":"Human-in-the-loop | Workflows","description":"Pause Mastra workflows for approvals, reviews, or user input, then resume with supplied data, reject with bail, or support repeated human interactions.","source":"@site/src/content/en/docs/workflows/human-in-the-loop.mdx","sourceDirName":"workflows","slug":"/workflows/human-in-the-loop","permalink":"/docs/workflows/human-in-the-loop","draft":false,"unlisted":false,"editUrl":"https://github.com/mastra-ai/mastra/tree/main/docs/src/content/en/docs/workflows/human-in-the-loop.mdx","tags":[],"version":"current","frontMatter":{"title":"Human-in-the-loop | Workflows","description":"Pause Mastra workflows for approvals, reviews, or user input, then resume with supplied data, reject with bail, or support repeated human interactions.","packages":["@mastra/core"]},"sidebar":"docsSidebar","previous":{"title":"Suspend and Resume","permalink":"/docs/workflows/suspend-and-resume"},"next":{"title":"Time Travel","permalink":"/docs/workflows/time-travel"}}'),a=t(74848),r=t(28453);let o={title:"Human-in-the-loop | Workflows",description:"Pause Mastra workflows for approvals, reviews, or user input, then resume with supplied data, reject with bail, or support repeated human interactions.",packages:["@mastra/core"]},i="Human-in-the-loop",u={},l=[{value:"Pausing workflows for human input",id:"pausing-workflows-for-human-input",level:2},{value:"Providing user feedback",id:"providing-user-feedback",level:2},{value:"Example output",id:"example-output",level:3},{value:"Resuming workflows with human input",id:"resuming-workflows-with-human-input",level:2},{value:"Handling human rejection with <code>bail()</code>",id:"handling-human-rejection-with-bail",level:3},{value:"Multi-turn human input",id:"multi-turn-human-input",level:2},{value:"Related",id:"related",level:2}];function p(e){let n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",mermaid:"mermaid",p:"p",pre:"pre",ul:"ul",...(0,r.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(n.header,{children:(0,a.jsx)(n.h1,{id:"human-in-the-loop",children:"Human-in-the-loop"})}),"\n",(0,a.jsxs)(n.p,{children:["Some workflows need to pause for human input before continuing. When a workflow is ",(0,a.jsx)(n.a,{href:"/docs/workflows/suspend-and-resume#pausing-a-workflow-with-suspend",children:"suspended"}),", it can return a message explaining why it paused and what\u2019s needed to proceed. The workflow can then either ",(0,a.jsx)(n.a,{href:"#resuming-workflows-with-human-input",children:"resume"})," or ",(0,a.jsx)(n.a,{href:"#handling-human-rejection-with-bail",children:"bail"})," based on the input received. This approach works well for manual approvals, rejections, gated decisions, or any step that requires human oversight."]}),"\n",(0,a.jsx)(n.h2,{id:"pausing-workflows-for-human-input",children:"Pausing workflows for human input"}),"\n",(0,a.jsxs)(n.p,{children:["Human-in-the-loop (HITL) input works much like ",(0,a.jsx)(n.a,{href:"/docs/workflows/suspend-and-resume",children:"pausing a workflow"})," using ",(0,a.jsx)(n.code,{children:"suspend()"}),". The key difference is that when human input is required, you can return ",(0,a.jsx)(n.code,{children:"suspend()"})," with a payload that provides context or guidance to the user on how to continue."]}),"\n",(0,a.jsx)(n.mermaid,{value:'flowchart LR\n accTitle: Suspending a workflow for human input\n accDescr: The workflow starts step1. The step either completes the workflow or suspends while it waits for human input.\n start(( start )) -- in --\x3e step1([step1])\n step1 -- out --\x3e stop(( end ))\n step1 pending1@-. suspend .-> paused@{ shape: manual-input, label: "awaiting<br/>human input" }\n\n class stop accent\n class paused pending'}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-typescript",metastring:'{12-17,22-26} title="src/mastra/workflows/test-workflow.ts"',children:"import { createWorkflow, createStep }
1 from '@mastra/core/workflows'\nimport { z } from 'zod'\n\nconst step1 = createStep({\n id: 'step-1',\n inputSchema: z.object({\n userEmail: z.string(),\n }),\n outputSchema: z.object({\n output: z.string(),\n }),\n resumeSchema: z.object({\n approved: z.boolean(),\n }),\n suspendSchema: z.object({\n reason: z.string(),\n }),\n execute: async ({ inputData, resumeData, suspend }) => {\n const { userEmail } = inputData\n const { approved } = resumeData ?? {}\n\n if (!approved) {\n return await suspend({\n reason: 'Human approval required.',\n })\n }\n\n return {\n output: `Email sent to ${userEmail}`,\n }\n },\n})\n\nexport const testWorkflow = createWorkflow({\n id: 'test-workflow',\n inputSchema: z.object({\n userEmail: z.string(),\n }),\n outputSchema: z.object({\n output: z.string(),\n }),\n})\n .then(step1)\n .commit()\n"})}),"\n",(0,a.jsx)(n.h2,{id:"providing-user-feedback",children:"Providing user feedback"}),"\n",(0,a.jsxs)(n.p,{children:["When a workflow is suspended, you can access the payload returned by ",(0,a.jsx)(n.code,{children:"suspend()"})," by identifying the suspended step and reading its ",(0,a.jsx)(n.code,{children:"suspendPayload"}),"."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-typescript",metastring:'{12} title="src/test-workflow.ts"',children:"const workflow = mastra.getWorkflow('testWorkflow')\nconst run = await workflow.createRun()\n\nconst result = await run.start({\n inputData: {\n userEmail: '[email protected]',\n },\n})\n\nif (result.status === 'suspended') {\n const suspendStep = result.suspended[0]\n const suspendedPayload = result.steps[suspendStep[0]].suspendPayload\n\n console.log(suspendedPayload)\n}\n"})}),"\n",(0,a.jsx)(n.h3,{id:"example-output",children:"Example output"}),"\n",(0,a.jsx)(n.p,{children:"The data returned by the step can include a reason and help the user understand what's needed to resume the workflow."}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-typescript",children:"{\n reason: 'Confirm to send email.'\n}\n"})}),"\n",(0,a.jsx)(n.h2,{id:"resuming-workflows-with-human-input",children:"Resuming workflows with human input"}),"\n",(0,a.jsxs)(n.p,{children:["As with ",(0,a.jsx)(n.a,{href:"/docs/workflows/suspend-and-resume#restarting-a-workflow-with-resume",children:"restarting a workflow"}),", use ",(0,a.jsx)(n.code,{children:"resume()"})," with ",(0,a.jsx)(n.code,{children:"resumeData"})," to continue a workflow after receiving input from a human. The workflow resumes from the step where it was paused."]}),"\n",(0,a.jsx)(n.mermaid,{value:'flowchart LR\n accTitle: Resuming a workflow with human input\n accDescr: Human input resumes step1. The step then completes the workflow.\n paused@{ shape: manual-input, label: "human input" }\n paused pending1@-. resume .-> step1([step1])\n step1 -- out --\x3e stop(( end ))\n\n class paused pending\n class stop accent'}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-typescript",metastring:"{13}",children:"const workflow = mastra.getWorkflow('testWorkflow')\nconst run = await workflow.createRun()\n\nawait run.start({\n inputData: {\n userEmail: '[email protected]',\n },\n})\n\nconst handleResume = async () => {\n const result = await run.resume({\n step: 'step-1',\n resumeData: { approved: true },\n })\n}\n"})}),"\n",(0,a.jsxs)(n.h3,{id:"handling-human-rejection-with-bail",children:["Handling human rejection with ",(0,a.jsx)(n.code,{children:"bail()"})]}),"\n",(0,a.jsxs)(n.p,{children:["Use ",(0,a.jsx)(n.code,{children:"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 ",(0,a.jsx)(n.code,{children:"success"})," status, and any logic after the call to ",(0,a.jsx)(n.code,{children:"bail()"})," is skipped."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-typescript",metastring:"{6-10}",children:"const step1 = createStep({\n execute: async ({ inputData, resumeData, suspend, bail }) =>
1 {\n const { userEmail } = inputData\n const { approved } = resumeData ?? {}\n\n if (approved === false) {\n return bail({\n reason: 'User rejected the request.',\n })\n }\n\n if (!approved) {\n return await suspend({\n reason: 'Human approval required.',\n })\n }\n\n return {\n message: `Email sent to ${userEmail}`,\n }\n },\n})\n"})}),"\n",(0,a.jsx)(n.h2,{id:"multi-turn-human-input",children:"Multi-turn human input"}),"\n",(0,a.jsxs)(n.p,{children:["For workflows that require input at multiple stages, the suspend pattern remains the same. Each step defines a ",(0,a.jsx)(n.code,{children:"resumeSchema"}),", and ",(0,a.jsx)(n.code,{children:"suspendSchema"})," typically with a reason that can be used to provide user feedback."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-typescript",metastring:'{11-16,21-25} title="src/mastra/workflows/test-workflow.ts" oxfmt:false',children:'const step1 = createStep({...});\n\nconst step2 = createStep({\n id: "step-2",\n inputSchema: z.object({\n message: z.string()\n }),\n outputSchema: z.object({\n output: z.string()\n }),\n resumeSchema: z.object({\n approved: z.boolean()\n }),\n suspendSchema: z.object({\n reason: z.string()\n }),\n execute: async ({ inputData, resumeData, suspend }) => {\n const { message } = inputData;\n const { approved } = resumeData ?? {};
1\n\n if (!approved) {\n return await suspend({\n reason: "Human approval required."\n });\n }\n\n return {\n output: `${message} - Deleted`\n };\n }\n});\n\nexport const testWorkflow = createWorkflow({\n id: "test-workflow",\n inputSchema: z.object({\n userEmail: z.string()\n }),\n outputSchema: z.object({\n output: z.string()\n })\n})\n .then(step1)\n .then(step2)\n .commit();\n'})}),"\n",(0,a.jsxs)(n.p,{children:["Each step must be resumed in sequence, with a separate call to ",(0,a.jsx)(n.code,{children:"resume()"})," for each suspended step. This approach helps manage multi-step approvals with consistent UI feedback and clear input handling at each stage."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-typescript",metastring:"{4,11}",children:"const handleResume = async () => {\n const result = await run.resume({\n step: 'step-1',\n resumeData: { approved: true },\n })\n}\n\nconst handleDelete = async () => {\n const result = await run.resume({\n step: 'step-2',\n resumeData: { approved: true },\n })\n}\n"})}),"\n",(0,a.jsx)(n.h2,{id:"related",children:"Related"}),"\n",(0,a.jsxs)(n.ul,{children:["\n",(0,a.jsx)(n.li,{children:(0,a.jsx)(n.a,{href:"/docs/workflows/control-flow",children:"Control Flow"})}),"\n",(0,a.jsx)(n.li,{children:(0,a.jsx)(n.a,{href:"/docs/workflows/suspend-and-resume",children:"Suspend and Resume"})}),"\n"]})]})}function c(e={}){let{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,a.jsx)(n,{...e,children:(0,a.jsx)(p,{...e})}):p(e)}},28453(e,n,t){t.d(n,{R:()=>o,x:()=>i});var s=t(96540);let a={},r=s.createContext(a);function o(e){let n=s.useContext(r);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:o(e.components),s.createElement(r.Provider,{value:n},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.