Mastra: hosted tools and human approval
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.
Prerequisites
Complete the 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 for the adapter config and 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.
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.
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:
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.
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() waits for a decision and then runs your own function with your own credential.
See the full Mastra example, receipts and grades, and the TypeScript SDK.

