HITL: Ask Before Acting

·

Oct 28, 2025

Mastra now supports HITL (Human-in-the-Loop) for tool approval. This allows you to safely build complex workflows that require human approval.

Good agents, like good teammates, know when they need input: before messaging 20 people across time zones, before booking the more expensive venue, or before deleting old records. They do the prep work—finding options, drafting content, checking budgets—then pause for your approval.

HITL makes this natural. Your agents handle the heavy lifting, then check in at the right moments. It's like working with a teammate who knows when a decision needs a second pair of eyes.

Tools also sometimes need human approval for compliance and security concerns.

How it works

Add requireApproval: true to any tool that needs permission:

 1const deleteTool = createTool({
 2  id: "delete-data",
 3  description: "Delete records from database",
 4  inputSchema: z.object({
 5    count: z.number(),
 6    table: z.string()
 7  }),
 8  execute: async ({ context }) => {
 9    return await deleteRecords(context);
10  },
11  requireApproval: true,
12});

When the agent tries to execute this tool, the stream pauses and waits for approval:

 1const stream = await myAgent.stream('Delete old user records');
 2
 3// Approve the tool call
 4const resumedStream = await myAgent.approveToolCall({ runId: stream.runId });
 5
 6// Or decline it
 7await myAgent.declineToolCall({ runId: stream.runId });

Conditional Approvals

Of course, you don't need approval for everything. That's why tools can suspend themselves based on conditions:

 1const transferTool = createTool({
 2  id: "transfer-money",
 3  execute: async ({ context, suspend, resumeData }) => {
 4    // Only suspend for large amounts
 5    if (context.amount > 1000 && !resumeData) {
 6      return await suspend({ 
 7        reason: `Transfer of $${context.amount} requires approval` 
 8      });
 9    }
10    
11    // Continue with approval data
12    if (resumeData?.approved) {
13      await executeTransfer(context);
14      return `Transfer completed`;
15    }
16    
17    return "Transfer cancelled";
18  },
19  suspendSchema: z.object({ reason: z.string() }),
20  resumeSchema: z.object({ approved: z.boolean() })
21});

Resume the suspended tool with approval:

 1const resumedStream = await myAgent.resumestream(
 2  { approved: true },
 3  { runId: stream.runId }
 4);

Your agents can now take real actions safely. They delete, transfer, and update—they just ask first when they should.

Check out Abhi's tweetstorm for more details. Happy building 🚀

Stay up to date