Full walkthrough: Human-in-the-loop for Mastra. Reaching your own end-users on their phones is the Pushary Partner plan.
Human-in-the-loop for Mastra. Ask a real human to approve, and get the answer on their phone. Two seams, pick by wait length:
- A blocking
createToolfor an approval that resolves in well under a minute. - A durable workflow step (
suspend/resume) for long waits. Mastra persists the snapshot, so the wait holds no idle compute and survives a restart.
Requires the Pushary Partner plan
and @mastra/core v1.
npm i @pushary/mastra @mastra/core zodSet PUSHARY_API_KEY (get it in your dashboard).
import { connect } from '@pushary/mastra'
const { universalLink } = await connect({ apiKey: process.env.PUSHARY_API_KEY! }, user.id)import { createPusharyAskTool } from '@pushary/mastra'
import { Agent } from '@mastra/core/agent'
const askHuman = createPusharyAskTool({ apiKey: process.env.PUSHARY_API_KEY! }, { externalId: user.id })
const agent = new Agent({
name: 'Support',
instructions: 'Call ask-human before any refund.',
model,
tools: { askHuman },
})The tool blocks until the person answers and returns { approved, value, status },
fail-closed. externalId is bound in code, never taken from model input.
import { pusharyApprovalStep } from '@pushary/mastra'
import { createWorkflow } from '@mastra/core/workflows'
import { z } from 'zod'
const approval = pusharyApprovalStep(
{ apiKey: process.env.PUSHARY_API_KEY! },
{ callbackUrl: `${process.env.PUBLIC_URL}/api/pushary/callback` },
)
export const refund = createWorkflow({
id: 'refund',
inputSchema: z.object({ question: z.string(), externalId: z.string() }),
outputSchema: z.object({ approved: z.boolean(), value: z.string() }),
})
.then(approval)
.commit()Run it, and it suspends at the approval step. Resume from the callback route:
import { resolvePusharyCallback } from '@pushary/mastra'
// POST /api/pushary/callback
export async function POST(req: Request) {
const raw = await req.text()
const cb = resolvePusharyCallback(raw, req.headers.get('x-pushary-signature'), process.env.PUSHARY_WEBHOOK_SECRET!)
if (!cb) return new Response('bad signature', { status: 401 })
const runId = await lookupRun(cb.correlationId) // your own correlationId -> runId map
const run = await refund.createRun({ runId })
await run.resume({ step: approval, resumeData: { answer: cb.answer } })
return new Response('ok')
}connect(config, externalId)— enroll an end-user's phone.createPusharyAskTool(config, { externalId })— a MastracreateToolthat blocks on a human.pusharyApprovalStep(config, { callbackUrl })— a durablecreateStepwith suspend/resume.resolvePusharyCallback(raw, signature, secret)— verify + parse a callback into{ correlationId, answer, approved, ... }.askExternalUser,createDurableDecision,describeAnswer,isAffirmative,deterministicKey,SIGNATURE_HEADER.
A runnable example is in examples/.
MIT