Mastra
Phone approvals for Mastra agents. Your agent asks, your user taps Approve or Deny.
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
finds that suspended run, asks your user on their phone, and approves or declines the
call so the run finishes.
Install
npm i @pushary/mastra @mastra/core zod
export PUSHARY_API_KEY=pk_xxx.sk_xxxGet the key at Partner onboarding. It needs Node.js 22.13 or later, like Mastra itself.
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.
Require approval before a tool runs
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 },
)
}When the model calls issueRefund, Mastra suspends the run instead of executing it.
resolvePusharyApprovals asks your user, one call at a time, and resumes the run
with their answer. Approve runs the refund. Deny, or no answer in time, declines it.
Each resumed reply is in outcome.resolved. Anything still waiting is in
outcome.pending.
Suspended runs live in Mastra's storage, so give your Mastra instance a storage
adapter. refundOrder and conversation are your own. policy: false means a
person always decides, even if you later add rules that could answer on their own.
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.
Take the user id from your signed-in user, never from the model's tool input.
Source and more examples: pushary-mastra on GitHub. Mastra reference: agent approval.