Mastra tool approval: requireToolApproval, requireApproval and what happens on a crash

Mastra can pause an agent before a tool runs and wait for someone to approve or decline. It has two switches, a dynamic form that fails closed, and one requirement people miss: storage.

The short answer

Set requireApproval: true on a tool made with createTool() to pause every call to that tool, or pass requireToolApproval: true to stream() or generate() to pause every tool call in that request. If either says so, the call pauses.1 The stream then emits a tool-call-approval chunk with toolCallId, toolName and args; you answer with approveToolCall() or declineToolCall() and the run’s runId.1

Two details matter most: requireToolApproval can be a function that decides per call, and if that function throws, Mastra defaults to requiring approval. And approvals use snapshots, so without a storage provider you get a “snapshot not found” error.1

Every MCP tool call goes through your rules first. Here a delete that doesn’t say which rows is stopped before it reaches the database. These are the rules deedglass init starts with; the call is made up.

Two switches, one rule

Switch Where Scope
requireApproval On the tool, in createTool() Every call to that tool
requireToolApproval On a stream() or generate() call Every tool call in that request

They combine with OR: if either is true, the call pauses.1 So you can mark dangerous tools once, at definition, and still turn on approval for everything during a sensitive request, such as one acting on production data.

Approving and declining

For stream():1

  1. Read chunks until a tool-call-approval arrives. It carries toolCallId, toolName and args.
  2. Show the reviewer the tool and the full arguments.
  3. Call approveToolCall() or declineToolCall() with the stream’s runId. Pass the toolCallId too when several calls may be pending.

For generate(), the matching calls are approveToolCallGenerate() and declineToolCallGenerate(); generate() returns with finishReason: 'suspended' while it waits.1

Declining takes an optional reason, which the model receives in place of the tool result.1 Use it. A decline with “Refunds over 100 need a manager; tell the customer it’s escalated” gives the model a next step. A bare decline invites it to try again.

Deciding per call

requireToolApproval can be a function, sync or async, that receives toolName, args, requestContext and workspace and returns true or false.1 A sketch, following the documented parameter names (we didn’t run it):

Code Program code, for developers. The text around it says what it does.

const stream = await agent.stream(messages, {
  requireToolApproval: ({ toolName, args }) =>
    toolName === "refund" ? Number(args.amount) > 100 : false,
});

Three rules from the docs shape how this behaves:1

  • If the function throws, approval is required. A bug in your rule doesn’t let calls through.
  • A tool-level requireApproval function takes precedence over the combined result.
  • Durable and stored agents accept only a boolean. A function there falls back to requiring approval.

The first rule is the right default, and not every framework has it. In Cedar, for example, a policy that errors is skipped, which can let a permit win; our policy language comparison walks through that case. Mastra’s choice means a malformed argument that crashes your rule ends with a person asked, not a refund sent.

Storage is not optional

Approval works by saving a snapshot of the run while it waits. From the docs:1

  • Without a storage provider, you’ll see “snapshot not found.”
  • Agent-run snapshots are minimal and deleted when the run finishes.
  • Suspended runs survive a restart only with a persistent storage provider; listSuspendedRuns() finds pending runs in storage again.

In practice: an in-memory setup works on your laptop and loses every pending approval when the server restarts. If approvals can wait overnight, configure persistent storage before you ship, and decide what happens to approvals nobody answers (expire them, and tell the user).

Test the approval path

Approval code tends to be tested only on the happy path. Before shipping, run four cases against a test agent with a mock or cheap model:

  1. A call that shouldn’t pause (a small refund): the tool runs and no tool-call-approval chunk appears.
  2. A call that should pause: the chunk appears with the arguments the model chose, and the tool hasn’t run.
  3. Decline with a reason: the model’s next message reflects the reason instead of retrying the same call.
  4. Restart while paused: with persistent storage, listSuspendedRuns() finds the run again, and approving it after the restart completes the call.

Case 4 is the one that fails in production when storage was never configured. It’s cheaper to find on day one.

Approval or suspend

Mastra has a second pause mechanism. A tool can call suspend() inside its execute function to pause mid-run; the stream emits tool-call-suspended, and you resume with resumeStream() and data matching the tool’s resumeSchema.1

Approval Suspend
Pauses Before the tool runs Inside the tool, wherever it calls suspend()
Answer Approve or decline Data matching resumeSchema
Auto-resume from the user’s next message No; needs an explicit decision Yes, with autoResumeSuspendedTools1
Use for “Should this run at all?” “I need more information to continue”

Keep high-impact actions on approval. Auto-resume from a chat message is convenient for “which account did you mean?”, and the wrong control for “may I send this money?”.

A checklist

  • Tools that write, send or spend have requireApproval.
  • Sensitive requests add requireToolApproval, as a function where amounts vary.
  • Persistent storage configured; pending approvals survive restarts.
  • toolCallId passed when approving, for runs with several pending calls.
  • Declines carry a reason the model can act on.
  • Unanswered approvals expire and the user is told.
  • Approval decisions logged with reviewer, tool and arguments.

Approvals live in your app. For rules that also hold for other clients, Deedglass’s docs include a Mastra recipe that routes the agent’s tools through its MCP server, one of nine framework recipes of 15 lines or fewer, with no live model run behind them.2 For the same pattern elsewhere, see AI SDK toolApproval and OpenAI Agents SDK approvals; the human-in-the-loop approval entry covers the idea. To see what the agents configured on your machine can change, paste the config into the check page or run npx deedglass scan (how the grade works); to try approval rules in your browser, open the playground.

Written with AI assistance and checked against our claims register.

The takeaway

Mastra pauses a tool call if either approval switch says so; a throwing approval function means ask first; without storage the paused run is lost.

Read nextOpenAI Agents SDK human-in-the-loop: pausing a run for approval with needs_approval Try it: install in 7 s

Related on this site

More posts

Sources

  1. 1Mastra docs: Agent approval · mastra.ai · checked 26 October 2026
  2. 2Deedglass docs: Mastra · docs.deedglass.com · checked 26 October 2026

Published 26 October 2026, sources last checked 26 October 2026. If something here is out of date, write to hello@deedglass.com.

Try it on your own setup

npx deedglass scan

Grades only your own setup. Nothing is uploaded.

Pricing