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:
const deleteTool = createTool({
id: "delete-data",
description: "Delete records from database",
inputSchema: z.object({
count: z.number(),
table: z.string()
}),
execute: async ({ context }) => {
return await deleteRecords(context);
},
requireApproval: true,
});When the agent tries to execute this tool, the stream pauses and waits for approval:
const stream = await myAgent.stream('Delete old user records');
// Approve the tool call
const resumedStream = await myAgent.approveToolCall({ runId: stream.runId });
// Or decline it
await 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:
const transferTool = createTool({
id: "transfer-money",
execute: async ({ context, suspend, resumeData }) => {
// Only suspend for large amounts
if (context.amount > 1000 && !resumeData) {
return await suspend({
reason: `Transfer of $${context.amount} requires approval`
});
}
// Continue with approval data
if (resumeData?.approved) {
await executeTransfer(context);
return `Transfer completed`;
}
return "Transfer cancelled";
},
suspendSchema: z.object({ reason: z.string() }),
resumeSchema: z.object({ approved: z.boolean() })
});Resume the suspended tool with approval:
const resumedStream = await myAgent.resumestream(
{ approved: true },
{ runId: stream.runId }
);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 🚀
