# Mastra

Source: https://pushary.com/docs/agents/build/mastra
Last updated: 2026-10-04

> 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.

```bash
npm i @pushary/mastra @mastra/core zod
export PUSHARY_API_KEY=pk_xxx.xxx
```

Get the key at [Partner onboarding](https://pushary.com/onboarding/partner). 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 [#set-up-mastra-human-in-the-loop]

<Steps>
  <Step>
    ### Connect a user once [#connect-a-user-once]

    ```ts
    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.
  </Step>

  <Step>
    ### Mark the tool and resolve the suspended run [#mark-the-tool-and-resolve-the-suspended-run]

    ```ts
    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.
  </Step>

  <Step>
    ### Run it and answer on the phone [#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`.
  </Step>
</Steps>

## What happens when nobody answers [#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](#wait-minutes-or-hours-for-an-answer).

## Let the model ask a question [#let-the-model-ask-a-question]

```ts
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 [#production-notes]

### Make sure a person always decides [#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 [#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 [#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 [#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 [#wait-minutes-or-hours-for-an-answer]

`resolvePusharyApprovals` waits inside one request. For later answers, use deferred reviews:

1. `createPusharyDeferredAskTool` for questions, and `requireApproval: true` for actions.
2. `openPusharyAgentReview` after Mastra suspends. It saves the exact call and customer binding, then asks.
3. `resumePusharyAgentReview` from 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](https://github.com/Pushary/pushary-mastra) covers the store contract. The [restart and live-phone recipe](https://github.com/Pushary/pushary-mastra/blob/main/examples/durable-agent.mjs) 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 [#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](https://github.com/Pushary/pushary-mastra). Mastra reference: [agent approval](https://mastra.ai/docs/agents/agent-approval).

## Next steps [#next-steps]

<Cards>
  <Card title="Try a simulated decision" description="Approve or deny a refund in your browser before you connect a phone." href="/try" />

  <Card title="See the Mastra approval workflow" description="The whole flow for Mastra agents, from connect to resume." href="/human-in-the-loop-mastra" />

  <Card title="Test your integration" description="Use a sandbox key to check approve, deny and no answer without a phone." href="/docs/agents/embed-testing" />
</Cards>