Framework guides

How to add human approvals to Mastra agents and workflows

Add Contro1 approval gates to Mastra agents and workflows so risky tool calls, workflow steps, and production actions wait for signed human decisions.

Mastra supports agents, tools, and workflows; Contro1 gives those action points role-based approvals, escalation, callbacks, and audit evidence.

Use the integration skill

Copy this skill link into your code agent to add Mastra and Contro1 to your system.

Copy skill link

Key takeaways

  • Start with one approval call before a risky Mastra tool executes.
  • Use a full production wrapper when you need routing checks, signed callbacks, follow-up logs, and evidence export.
  • Control Map previews who can approve right now by role, shift coverage, fallback reviewers, and quorum requirements.
  • Use audit records for actions that were allowed to run, and evidence packets when compliance needs the full decision trail.

Connect your Mastra agent to Contro1

A Mastra agent runs in your own code, so it connects with an Agent Credential: a key bound to one agent, so every call is attributed to it and nothing in a request can change which agent it is.

  • 1. Create the agent: Agents, New agent, "An agent you are building for production" (or contro1 init --name "<name>" --framework mastra). You are its accountable owner.
  • 2. Connect the application account under Apps, if it is not connected yet.
  • 3. Give the agent the Actions it needs under Access. It starts with none.
  • 4. Create an Agent Credential for it (Settings, Agent keys) and store it as CONTRO1_API_KEY in your secret manager.
  • 5. Call Actions from your tools as below. The same credential creates approval requests for work your own code does.

Connect an agent: every step, in full

Run an application Action from a Mastra tool

Contro1 holds the account and makes the call, so the record is what Contro1 observed. The tool returns what the Action produced; when a person has to approve first, it waits and then returns the result. It never re-submits: a retry could send a second email.

Before writing the input, read the exact input_schema with get_action_contract, or from the Action on the Access page.

tools/listRecentEmails.ts
// npm install @contro1/sdk@^1.5.0
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
import { CentcomClient, ActionsApi, needsHumanResolution } from "@contro1/sdk";

const actions = new ActionsApi(new CentcomClient({ apiKey: process.env.CONTRO1_API_KEY! }));

async function runAction(action_id: string, input: Record<string, unknown>) {
  const out = await actions.invoke({
    action_id, input,
    authority_mode: "agent_principal",
    account_mode: "shared",
    idempotency_key: crypto.randomUUID(),
  });
  if (out.invocation.state === "executed") return out.result;
  if (out.invocation.state === "awaiting_approval") {
    const settled = await actions.waitForInvocation(out.invocation.invocation_id);
    if (needsHumanResolution(settled)) throw new Error("Outcome unknown; a person must check. Do not retry.");
    return actions.getResult(settled.invocation_id);
  }
  throw new Error(`Not run: ${out.invocation.state}`);
}

export const listRecentEmails = createTool({
  id: "list-recent-emails",
  description: "List the most recent emails in the team mailbox.",
  inputSchema: z.object({ max_results: z.number().int().min(1).max(50).default(5) }),
  execute: async ({ context }) => runAction("gmail.message.list", { max_results: context.max_results }),
});

What this integration does

A Mastra agent can decide to call tools, and a Mastra workflow can run known production steps. Contro1 sits immediately before the risky action. The agent asks Contro1 for a human decision, Contro1 routes the request to the right role, and the action runs only after an approved signed result.

To get started, wire up a single tool first, such as deploy, send_email, issue_refund, update_crm, grant_access, delete_data, or transfer_money. Confirm the whole flow works and is configured correctly, then move on to more tools and add Control Map, action logs, and evidence export.

Setup

There is no Mastra-specific npm package required for this first integration. The examples use direct HTTPS calls to Contro1 so you can add approvals to an existing Mastra project immediately.

If your project already uses the Contro1 TypeScript SDK, you can replace the fetch calls with the SDK client. The required runtime configuration stays the same.

.env
CENTCOM_API_KEY=cc_live_your_key
CENTCOM_BASE_URL=https://api.contro1.com/api/centcom/v1
CENTCOM_WEBHOOK_SECRET=whsec_your_secret
CENTCOM_CALLBACK_URL=https://your-app.example.com/webhooks/contro1

Short example: ask for approval

This is the smallest useful pattern. Put it at the top of a risky Mastra tool before the real business action runs.

approve.ts
async function askForApproval(runId: string, toolId: string, input: unknown) {
  const request = await fetch('https://api.contro1.com/api/centcom/v1/requests', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.CENTCOM_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      type: 'approval',
      question: `Approve Mastra tool: ${toolId}?`,
      context: input,
      callback_url: 'https://your-app.example.com/webhooks/contro1',
      required_role: 'manager',
      external_request_id: `mastra:${runId}:${toolId}`,
      correlation_id: runId,
      metadata: { integration: 'mastra', tool_id: toolId },
    }),
  }).then((r) => r.json());

  return request.id;
}

Send context the reviewer can trust

In the short example, context: input copies the exact tool input your code intercepted - machine-observed fact the reviewer can trust. Add two more things: the trigger (the user message or event that started the run) and the agent's own justification, which you get reliably by making reason a required field of the tool's input schema so the model produces it at decision time.

Keep the two kinds apart in context: facts your code observed versus text the model wrote. Agent-written justification is agent-reported evidence: it helps the reviewer decide, but it must never change routing, risk_level, or approval policy, because a prompt-injected agent writes very persuasive reasons. If a high-risk request arrives without this context, fail closed and reject it instead of asking a human to guess.

Reviewer context and trust rules

Full example: production Mastra tool

A production integration normally has three parts: create the approval request, wait for a verified decision, then run the business action.

The example below uses a direct HTTP call so it works before you add a dedicated connector package. Replace waitForSignedContro1Decision with your webhook-backed decision store.

src/mastra/tools/deployRelease.ts
import { createTool } from '@mastra/core/tools';
import { z } from 'zod';

export const deployRelease = createTool({
  id: 'deploy-release',
  description: 'Deploy a release after human approval.',
  inputSchema: z.object({ version: z.string(), environment: z.string() }),
  execute: async ({ context }) => {
    const runId = process.env.MASTRA_RUN_ID ?? crypto.randomUUID();
    const toolId = 'deploy-release';
    const externalRequestId = `mastra:${runId}:${toolId}:${context.version}`;

    const request = await fetch('https://api.contro1.com/api/centcom/v1/requests', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.CENTCOM_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        type: 'approval',
        question: `Deploy ${context.version} to ${context.environment}?`,
        context,
        callback_url: process.env.CENTCOM_CALLBACK_URL,
        required_role: 'developer',
        priority: context.environment === 'production' ? 'urgent' : 'normal',
        external_request_id: externalRequestId,
        correlation_id: runId,
        metadata: { integration: 'mastra', tool_id: toolId, version: context.version },
      }),
    }).then((r) => r.json());

    const decision = await waitForSignedContro1Decision(request.id);
    if (!decision.response?.approved) throw new Error('Deploy rejected by operator');

    const result = await deploy(context.version, context.environment);

    return { result, contro1_request_id: request.id };
  },
});

If routing fails, check who is available

Most integrations do not need Control Map in the normal approval path. If a request cannot be routed, times out unexpectedly, or your app wants to show an operational error, call Control Map to see who is currently available.

It helps answer which required roles are mapped, who is on shift now, whether fallback reviewers exist, and which warning explains the routing problem.

src/mastra/controlMap.ts
const preview = await fetch('https://api.contro1.com/api/centcom/v1/requests/control-map', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CENTCOM_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    approval_requirements: {
      required_roles: ['developer'],
      required_approvals: 1,
    },
    metadata: {
      integration: 'mastra',
      workflow_id: 'release-deploy',
      step_id: 'production-deploy',
    },
  }),
}).then((r) => r.json());

console.log(preview.satisfiable);        // can this request be routed?
console.log(preview.on_shift_capacity);  // who is currently available?
console.log(preview.fallback_reviewers); // who can receive fallback routing?
console.log(preview.warnings);           // why routing may fail

API endpoint index · Requests API

Workflow approval step

If the risky action is part of a deterministic Mastra workflow, keep the approval as its own step between prepare and execute. The workflow state should carry the run ID, request ID, reviewer comment, and final decision.

src/mastra/workflows/approvalStep.ts
const approvalRequest = {
  type: 'approval',
  question: 'Approve CRM write-back?',
  context: 'Write 240 qualified leads to production CRM.',
  callback_url: process.env.CENTCOM_CALLBACK_URL,
  required_role: 'manager',
  external_request_id: `mastra:${runId}:crm-write`,
  correlation_id: runId,
  metadata: {
    integration: 'mastra',
    workflow_id: 'lead-enrichment',
    step_id: 'crm-write',
  },
};

Mastra docs · Requests API · Webhooks

Log autonomous and post-approval actions

Every approval request already stores the human decision in Contro1. You do not need to create an audit record just to prove the approval happened.

Use POST /audit-records for actions that did not need approval, and optionally after an approved action completes if you want to record what your Mastra system actually did next.

log_allowed_action.ts
await fetch('https://api.contro1.com/api/centcom/v1/audit-records', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CENTCOM_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    action: 'mastra.search_completed',
    summary: 'Searched internal docs for refund policy',
    source: { integration: 'mastra', workflow_id: 'support-agent', run_id },
    outcome: 'success',
    severity: 'info',
    correlation_id: runId,
  }),
});
log_post_approval_action.ts
await fetch('https://api.contro1.com/api/centcom/v1/audit-records', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CENTCOM_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    action: 'mastra.crm_write_completed',
    summary: 'Wrote 240 qualified leads to production CRM after approval',
    source: { integration: 'mastra', workflow_id: 'lead-enrichment', run_id },
    outcome: 'success',
    correlation_id: runId,
    in_reply_to: { type: 'request', id: requestId },
  }),
});

Get evidence for compliance or incident review

Evidence is the exportable packet for one reviewed action. It includes the request metadata, reviewer decision, policy context, timestamps, callback delivery status, and final protocol response.

Use the request evidence endpoint when compliance asks for proof of one approval. Use the case endpoint when you need the full timeline of approvals and audit records for one Mastra run.

get_evidence.ts
const evidence = await fetch(
  `https://api.contro1.com/api/centcom/v1/requests/${requestId}/evidence`,
  { headers: { Authorization: `Bearer ${process.env.CENTCOM_API_KEY}` } },
).then((r) => r.json());
get_case.ts
const timeline = await fetch(
  `https://api.contro1.com/api/centcom/v1/cases/${runId}`,
  { headers: { Authorization: `Bearer ${process.env.CENTCOM_API_KEY}` } },
).then((r) => r.json());

Send full agent traceability

Beyond the approval call, attach a run trace, the tools you invoked, and the context you retrieved. Each field is optional; add what you have. Identity always comes from your credential. When a named part of a multi-agent system made the call, say which with the Contro1-Sub-Agent header (as_sub_agent in the SDKs): Contro1 registers it under this agent, and it never has more authority than this agent.

  • trace_id / parent_trace_id - link one run (and sub-agent runs) into a single trace.
  • tool_calls[] - what the agent tried to do, so reviewers see the actions. A hook can report every tool call for you.
  • retrieved_context[] - the data the decision was based on (RAG provenance).
  • Then export a signed evidence packet from GET /requests/:id/evidence.
Send full traceability
// Same approval call you already make - now with full traceability.
const billing = client.asSubAgent("billing"); // optional: a named part of this agent
await billing.createProtocolRequest({
  request_type: "approval",
  title: "Refund $4,200 to customer 8831",
  source: { integration: "mastra" },
  trace_id: `trc_${runId}`,          // link every step of this run
  tool_calls: [{ name: "lookup_order", outcome: "success" }],
  retrieved_context: [{ source: "policy:refunds", uri: "kb://policy/refunds" }],
  continuation: { mode: "decision" },
});

Agent identity, traceability & signed evidence

Make the approval the gate, not a suggestion

The signed webhook is cryptographic proof of a human decision. Verify it inside the system that executes the action - not inside the agent. Any tool that must never run without human sign-off (payments, deploys, data deletion) should refuse to act without a verified approval; that way no agent, including shadow agents nobody registered, can trigger it by skipping Contro1.

  • Verify the signature and reject timestamps older than 5 minutes (replay protection; the timestamp marks callback delivery, not request creation, so long SLAs are unaffected).
  • Bind the approval to the exact action parameters via metadata / correlation_id - never treat "an approval arrived" as permission for a different action.
  • Execute each request_id exactly once (idempotency on your side).
  • When in doubt, confirm state directly with GET /v1/requests/:id using a read-only API key.

Full guardrail pattern with code: Webhooks

Frequently asked questions

Should approval live in a tool or workflow step?

Put approval inside a tool when the agent chooses whether to call it. Put approval in a workflow step when the process is deterministic and the risky action is known in advance.

What should correlation_id be?

Use the Mastra run ID, workflow run ID, or another stable execution ID. Keep external_request_id scoped to the exact tool call or workflow step.

What does Control Map return?

It previews routing: whether the approval can be satisfied, which roles are mapped, who is currently available by shift or fallback, and whether quorum or separation-of-duties requirements can be met.

How do I prove what happened after approval?

Log the completed action with POST /audit-records and include in_reply_to with the Contro1 request id. This ties the human decision and the follow-up action into one case timeline.

How do I export evidence?

Use GET /requests/:id/evidence for one approved or denied action, and GET /cases/:case_id for the full timeline of approvals and audit records that share the same correlation_id.

Does this require a full SDK repo?

No. The starter repo can hold only the skill at first. The examples use direct HTTP calls so teams can adapt them to their Mastra runtime immediately.

Related resources

AI agent approvals and escalations

Design approval workflows, timeout handling, fallback reviewers, SLA escalation, and signed callback paths for production AI agent systems.