← Blog

Mastra human-in-the-loop
tool approval.

Short answer: set requireApproval on a Mastra tool when the agent should pause before that tool runs. Use nativeApprovals: true in nominee-mastra when the approval requirement comes from shared application policy and you want Mastra to own the suspend and resume experience.

Mastra’s current tool approval guide covers the native flow, including approving or declining a suspended call. Its human-in-the-loop guidance is a useful test for deciding which tools need that interruption.


Use Mastra alone for a tool-local gate

export const refundTool = createTool({
  id: 'refundPayment',
  description: 'Refund a payment',
  inputSchema: z.object({ transactionId: z.string() }),
  outputSchema: z.object({ status: z.literal('refunded') }),
  requireApproval: true,
  execute: ({ transactionId }) => payments.refund(transactionId),
})

This is sufficient when approval is a property of one Mastra tool and the rest of your application accepts the same rule. Mastra handles the tool-call suspension. Your application handles the user interface and the approve or decline response.

The rule becomes an application concern when it varies by refund amount, current user, account ownership, tenant, or budget. Those values should be supplied or verified by trusted server code, not inferred from the model’s tool arguments.


Let policy decide and Mastra pause

The adapter below creates a regular Mastra tool. A policy ask becomes Mastra’s native approval flow when the tool runs inside an agent:

import { Nominee, allow, ask } from 'nominee'
import { nomineeTool } from 'nominee-mastra'
import { z } from 'zod'

const nominee = new Nominee({
  policy: {
    rules: [
      allow('payments.refund', {
        when: ({ input }) => input.cents <= 2_000,
      }),
      ask('payments.refund'),
    ],
    fallback: 'deny',
  },
})

export const refundTool = nomineeTool({
  id: 'refundPayment',
  description: 'Refund a payment',
  inputSchema: z.object({
    transactionId: z.string(),
    cents: z.number().int().positive(),
  }),
  outputSchema: z.object({ status: z.literal('refunded') }),
  nominee,
  action: 'payments.refund',
  user: session.userId,
  resource: ({ input }) => 'payment:' + input.transactionId,
  nativeApprovals: true,
  execute: async ({ transactionId, cents }, { action }) => {
    await payments.refund({ transactionId, cents, idempotencyKey: action.id })
    return { status: 'refunded' as const }
  },
})

Add refundTool to the Mastra agent’s tools. Mastra calls its native approval hook before execute. After approval, the adapter records the trusted Mastra tool-call ID as approval evidence and executes the exact arguments through Nominee’s single-use action path.

Direct or workflow execution has no trusted agent tool-call ID. In that case the adapter does not self-approve; it falls back to Nominee’s portable pending-action flow. That preserves the same policy without pretending a native approval happened.

Pick the owner of the pause

  • Use requireApproval when one Mastra tool owns a static or local approval rule.
  • Use nativeApprovals: true when shared policy decides and Mastra should present and resume the interruption.
  • Use the portable pending-action flow for workers or workflows that need to resume outside an agent run.

Read native approval versus action authorization for the boundary between a framework’s pause mechanism and your application’s permission model.

See the boundary run

Approve one tool call and inspect the receipt chain.

$ npx nominee-cli proof runs offline after npx installs the package
Star on GitHub Read the approval docs →