Human-in-the-loop for Mastra

Human-in-the-loop for Mastra agents and workflows.

A Mastra tool or a durable workflow step that asks a real person and waits for the answer.

The integration

Add it to your Mastra agent

Install @pushary/mastra. It gives you two ready-made pieces. createPusharyAskTool is a Mastra tool that blocks until a person answers, for a quick approval. pusharyApprovalStep is a workflow step that suspends and resumes on a signed webhook, for a wait measured in hours. Either way the question lands on a phone, the wait is durable, and the decision comes back fail-closed. Connect each end-user's phone once with a one-tap link, and the externalId is your own id for that user.

npm i @pushary/mastratwo calls
import { createPusharyAskTool, connect } from "@pushary/mastra"

// Connect the user's phone once, keyed to your own id.
await connect({ apiKey: process.env.PUSHARY_API_KEY! }, "user_123")

// A Mastra tool that blocks on a real person, fail-closed.
export const askHuman = createPusharyAskTool(
  { apiKey: process.env.PUSHARY_API_KEY! },
  { externalId: "user_123" },
)
// add it to any Agent({ tools: { askHuman } })

Why you need it

Mastra pauses the agent. Pushary reaches the human.

Mastra can suspend a workflow step and persist the snapshot, and a suspended step does not tell anyone it is waiting. @pushary/mastra completes the loop. createPusharyAskTool covers the blocking case, and pusharyApprovalStep suspends the run, delivers the question to a phone, and resumes on Pushary's signed webhook. Mastra persists the snapshot, so an hour-long wait holds no compute and survives a restart, and every decision is fail-closed and logged.

What Mastra already gives you

suspend() and run.resume()

Mastra workflows suspend properly. A step calls suspend(), Mastra writes a snapshot to storage, and the run stops. When you call run.resume() with resumeData, the step continues with that data. Because the snapshot is persisted rather than held in memory, the wait can outlive the process that started it.

native
const approval = createStep({
  id: "approval",
  execute: async ({ suspend, resumeData }) => {
    if (!resumeData) return await suspend({})
    return { approved: resumeData.approved }
  },
})

Where it stops

  • A suspended step is silent. Storage knows the run is waiting and no human does.
  • run.resume() has to be called by something. You build the route that receives the answer and maps it back to the right runId.
  • The snapshot holds workflow state, not an audit record of who approved and what they were shown.

Before you ship it

Things that bite on Mastra

You need a correlationId to runId map
The answer arrives on a webhook that knows about a decision, not about your workflow. Store the mapping when you suspend, or you will have an answer and no idea which suspended run it belongs to. This is the piece people forget until the first real approval comes back.
resumeData is undefined on the first pass
The step executes once to suspend and again to resume, through the same execute function. Forgetting the undefined branch on the first pass throws before it ever suspends.
Agent tools and workflow steps are different tools for different waits
A Pushary ask tool inside an Agent blocks the request, which suits a wait of seconds. A workflow approval step suspends to storage, which suits a wait of hours. Reaching for the tool when you needed the step is how a request times out mid-approval.

How it works

From ask to resume

Connect the phone once, then ask a human from the tool or step that guards the action. The waiting, the delivery, and the record are handled for you.

  1. 01

    Install the adapter

    Run npm i @pushary/mastra and set your Pushary API key in the environment.

  2. 02

    Connect the phone

    Call connect(config, external_id) once per end-user. It returns a link the person taps once to turn on approvals. It opens the Pushary app, which is what carries Approve and Deny on the lock screen, and they need no account of ours.

  3. 03

    Add the tool or the step

    For a quick approval, add createPusharyAskTool to your Agent's tools. For a long wait, put pusharyApprovalStep in a workflow; it suspends until the person answers.

  4. 04

    Resume on the answer

    The tool returns fail-closed. The step resumes when Pushary's signed webhook arrives, so a decline, a timeout, or no reply never lets the workflow take the guarded step.

A decision is a yes or no confirm, a multiple-choice select, or a free-text input. Every one is stored with its outcome and kept in the audit log.

The Partner plan

Your agent asks your users, not just you

Ship the agent to real users and let each of them approve their own decisions from their phone, under your brand.

$99/ month, every framework included

Your users never see a Pushary login or bill.

Your users answer on their own phones

The externalId is your own id for the end-user. Connect their phone once with a keyless one-tap link that opens the Pushary app, and every approval lands with the right person. No account and no key on their side.

Durable, fail-closed waits

The agent stays open on a durable wait backed by a decisions ledger. If nobody answers in the window you set, the decision comes back denied, so a timeout or a restart never turns into an unapproved action.

Phone-first delivery

Pushary reaches the person on the device they carry: the app first, because it is the only channel that puts Approve and Deny on a lock screen, then PWA push notifications, then Slack if you route it there. You do not build a delivery pipeline.

Signed webhooks and an audit trail

Every decision is written to a durable ledger you can query and export, and resolved decisions post a signed webhook to your callback URL. You have the record of who approved what and when.

FAQ

Questions, answered

Is there a Mastra package for Pushary?
Yes. npm i @pushary/mastra gives you connect(), createPusharyAskTool for a blocking approval, and pusharyApprovalStep for a durable suspend/resume workflow step.
Should I use the tool or the workflow step?
Use createPusharyAskTool for a blocking approval that resolves in well under a minute. Use pusharyApprovalStep when the wait can be long: it suspends the run and resumes from a signed webhook, so it holds no idle compute.
What happens when nobody answers?
The decision comes back as not approved. Anything short of an explicit approval fails closed, so the workflow does not take the guarded action, and the durable ledger keeps the request across restarts.
Can each of my end-users approve on their own phone?
Yes. The externalId is your own id for the user, connected once with a one-tap link. Pass it per run, so each user decides about their own action, with no account or app on their side.
Does the wait survive a restart or a deploy?
Yes. The decision lives in Pushary's durable ledger, and a suspended Mastra step persists its snapshot to your configured storage, so both sides of the wait come back after a restart.
When should I use suspend() instead of a blocking ask tool?
Match it to how long the person will take. A Pushary ask tool inside an Agent blocks the request and suits a wait measured in seconds, with no storage wiring on your side. A workflow step that calls suspend() writes a snapshot and suits a wait measured in hours, because the run survives the process. The usual mistake is reaching for the tool and then watching the request time out halfway through a real approval.

Let your Mastra agent ask a human.

Connect a user with one tap, ask a human with one call, and act on a fail-closed decision. Every decision is saved to a durable ledger you can audit.

Partner plan, $99 a month. Your users never see a Pushary login or bill.