---
title: "Mastra: hosted tools and human approval"
description: "Call an Anlyon action from a Mastra tool, let an approver decide outside the agent, keep provider credentials out of the Mastra app, and read the receipt by invocation ID."
---

Mastra runs the agent. Anlyon executes the named external action. As of 2026-10-18, Mastra's `requireApproval` on `createTool()` and `requireToolApproval` on `stream()` or `generate()` pause a tool call in your app until your code calls `approveToolCall()` or `declineToolCall()`. They do not hold credentials, and approving a Mastra tool call does not approve a pending Anlyon invocation. See Mastra's [agent approval docs](https://mastra.ai/docs/agents/agent-approval).

## Prerequisites

Complete the [quickstart](/quickstart) in a test environment. Define a governed action, for example `update-docs-file` on the `github.file_update` adapter with `requiresApproval: true`, using an operator credential outside the app. See [Governed effects](/execution/governed-effects) for the adapter config and [Impact limits](/execution/impact-limits) to cap it.

The Mastra app needs an Anlyon agent credential with `actions:invoke` and `actions:read`. It does not need the provider token or `approvals:decide`.

```bash
npm install @anlyonhq/sdk @mastra/core zod
```

## A tool that invokes the action

The tool invokes the action and returns at once. An approval can take hours, so the tool does not hold the agent run open.

```typescript
import { createTool } from '@mastra/core/tools';
import { Client } from '@anlyonhq/sdk';
import { z } from 'zod';

const anlyon = new Client({ apiKey: process.env.ANLYON_AGENT_KEY! });

await anlyon.auth.requireScopes(['actions:invoke', 'actions:read'], {
  forbidden: ['actions:write', 'secrets:write', 'approvals:decide'],
});

export const updateDocsFile = createTool({
  id: 'update-docs-file',
  description: 'Replace the contents of one existing file under docs/. Every change is reviewed before it is committed.',
  inputSchema: z.object({
    changeId: z.string(),
    path: z.string(),
    content: z.string(),
    message: z.string(),
  }),
  execute: async ({ changeId, path, content, message }) => {
    const { data } = await anlyon.actions.invoke(
      'update-docs-file',
      { path, content, message },
      { operationKey: `docs:${changeId}` },
    );
    const invocation = data!;
    if (invocation.pendingApproval) {
      return { status: 'queued_for_review', invocationId: invocation.id };
    }
    return { status: invocation.status, grade: invocation.grade ?? null, invocationId: invocation.id };
  },
});
```

Register it on an agent with `tools: { updateDocsFile }`. Derive `operationKey` from your own record, not from the model. The same key and request return the existing effect instead of a second write.

## Reading the result

Persist the returned invocation ID. Read it from a job, a webhook handler or the next turn:

```typescript
const { data: invocation } = await anlyon.actions.invocation(invocationId);
console.log(invocation!.status, invocation!.grade);
```

| Status | Application behavior |
| --- | --- |
| `pending_approval` | Show the pending review. An approver decides in Anlyon |
| `running` | Read the same invocation later |
| `succeeded` | Report the receipt grade. `confirmed` means the commit was read back from GitHub |
| `unknown` | No usable response. Anlyon does not send it again. Do not submit another write |
| `failed` | Report it. For `github.file_update`, a file changed at GitHub after review is refused by GitHub on the reviewed blob SHA |
| `denied` or `expired` | Stop. Do not resubmit to bypass the decision |

A subscription can also receive `action.succeeded` and `action.failed`. See [Events and webhooks](/guides/webhooks).

## Mastra approvals and Anlyon approvals together

`requireApproval` suits a question to the user in the conversation. The Anlyon approval is the decision on the production call: bound to the exact request, decided by a credential other than the one that asked. Turning both on for one tool asks twice. Keep provider tokens out of the Mastra app, or a tool can call the provider directly and skip both.

A Mastra tool that calls a provider directly is not routed through Anlyon and is not governed by it. For a call you cannot move yet, [`approvals.gate()`](/guides/approval-gates) waits for a decision and then runs your own function with your own credential.

See the [full Mastra example](https://anlyon.com/examples/mastra-tool-approval), [receipts and grades](/execution/outcomes), and the [TypeScript SDK](/guides/typescript-sdk).
