PageSourceSearch

https://mastra.ai/assets/js/536ba795.c4ab6d44.js

js mastra.ai collected 2026-09-24 17:14:29 UTC 11,807 bytes, 1 lines download raw bytes

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.