Mastra
Mastra human in the loop with phone approvals. Tools marked requireApproval suspend, your user taps Approve or Deny, and the run resumes with the answer.
Mastra can hold a tool call until someone approves it. Set requireApproval: true on the tool and the agent run suspends before the tool executes. @pushary/mastra adds human in the loop to Mastra: it finds that suspended run, asks your user on their phone, and approves or declines the call so the run finishes.
npm i @pushary/mastra @mastra/core zod
export PUSHARY_API_KEY=pk_xxx.xxxGet the key at Partner onboarding. It needs Node.js 22.13 or later, like Mastra itself, and @mastra/core 1.59 or later below 2.
Set up Mastra human in the loop
Connect a user once
import { connect } from '@pushary/mastra'
const config = { apiKey: process.env.PUSHARY_API_KEY! }
const { universalLink } = await connect(config, user.id)Show the link to that user. They open it on their phone once, install the free Pushary app, and never sign up or pay.
Mark the tool and resolve the suspended run
import { Agent } from '@mastra/core/agent'
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'
import { resolvePusharyApprovals } from '@pushary/mastra'
const issueRefund = createTool({
id: 'issue-refund',
description: 'Refund an order',
inputSchema: z.object({ orderId: z.string(), amount: z.number().positive() }),
outputSchema: z.object({ refunded: z.boolean() }),
requireApproval: true,
execute: async ({ orderId, amount }) => refundOrder(orderId, amount),
})
const agent = new Agent({
id: 'refunds',
name: 'Refunds',
instructions: 'Refund orders when the customer asks.',
model: 'openai/gpt-4o',
tools: { issueRefund },
})
const output = await agent.generate('Refund order 1234 in full.', {
memory: { thread: conversation.id, resource: user.id },
})
if (output.finishReason === 'suspended') {
const outcome = await resolvePusharyApprovals(
{ externalId: user.id, policy: false },
{ agent, resourceId: user.id },
)
}refundOrder and conversation are your own. Suspended runs live in Mastra's storage, so give your Mastra instance a storage adapter.
Run it and answer on the phone
When the model calls issueRefund, Mastra suspends the run instead of executing it. resolvePusharyApprovals asks your user, one call at a time. They tap Approve, and the run resumes and the refund runs. Each resumed reply is in outcome.resolved.
What happens when nobody answers
The refund does not run. Each question waits up to 55 seconds. A deny or no answer in that time declines the call, and Mastra gives the model the reason:
| Outcome | Reason the model reads |
|---|---|
| Your user taps Deny | The approver denied this action. |
| Nobody answers in time | No answer from the approver, so this was denied. Do not retry the same action. |
To wait longer than one request, use deferred reviews.
Let the model ask a question
import { Agent } from '@mastra/core/agent'
import { createPusharyAskTool } from '@pushary/mastra'
const agent = new Agent({
id: 'support',
name: 'Support',
instructions: 'Ask the customer which order they need help with.',
model: 'openai/gpt-4o',
tools: { askHuman: createPusharyAskTool(config, { externalId: user.id }) },
})
await agent.generate('Find out which order the customer means.')The tool returns { approved, value, status } to the model. The model chooses when to call it, so use it for questions and keep requireApproval for actions that need a yes. A choice or a typed answer is data, not permission, even when the text is yes.
Production notes
Make sure a person always decides
policy: false means a person always decides. Without it, the shared gate asks your site's policy first, and a policy rule that names the tool can allow or deny it without paging anyone.
Read every result
resolvePusharyApprovals returns { resolved, pending, allApproved }:
| Field | What it holds |
|---|---|
resolved[].output | The resumed generation. Save each one. |
resolved[].reviewError | Routing, question or gate failure. The resume was not tried and the run is still pending. |
resolved[].resumeError | Mastra's resume was tried and failed. |
resolved[].discoveryError | A later suspension could not be found. Retry it without rerunning the action. |
pending | Runs still waiting. The resolver stops at maxApprovals (default 10), a data question or an error. |
allApproved | True only with no pending work and no errors. It does not prove your side effect succeeded. |
Scope it to one customer
Pass trusted runs, a threadId or a resourceId. A call with none of them is rejected, so the resolver never sweeps every suspended run in your app.
Pick the right person
Take the user id from your signed-in user, never from the model's tool input. A prompt injection could otherwise send the approval to someone else.
Wait minutes or hours for an answer
resolvePusharyApprovals waits inside one request. For later answers, use deferred reviews:
createPusharyDeferredAskToolfor questions, andrequireApproval: truefor actions.openPusharyAgentReviewafter Mastra suspends. It saves the exact call and customer binding, then asks.resumePusharyAgentReviewfrom a signed callback or a polling job. It reads the authoritative decision before it resumes the saved run.
Your application supplies persistent Mastra storage, an atomic review store, output storage and business idempotency. The public package guide covers the store contract. The restart and live-phone recipe needs no model key, moves no money and polls without a public callback server. Its approval, denial, choice and text checks run in separate processes and check duplicate handling.
Pushary records the answer. Your application records the continuation and the business result. If execution becomes uncertain, reconcile the saved run and the business receipt before you retry. A timeout alone never makes a retry safe.
Versions
@pushary/mastra 0.4 requires @pushary/server 2.1, @mastra/core 1.59 or later below 2, and Node.js 22.13 or later.
Source and more examples: pushary-mastra on GitHub. Mastra reference: agent approval.
Next steps
Try a simulated decision
Approve or deny a refund in your browser before you connect a phone.
See the Mastra approval workflow
The whole flow for Mastra agents, from connect to resume.
Test your integration
Use a sandbox key to check approve, deny and no answer without a phone.