Mastra
Suspend a Mastra agent tool or workflow step, review it in Hitly, resume or bail.
Mastra pauses with suspend() and continues with resume() / bail(). Hitly owns the reviewer UI. Chat with the demo agent in Mastra Studio (mastra dev, http://localhost:4111).
How it works with Hitly
- An agent tool or workflow step calls
suspend()and POSTs the payload to Hitly. - Hitly stores an envelope and shows it in the inbox.
- A reviewer accepts, edits, or rejects.
- Hitly resumes the origin:
- Workflow —
POST /api/workflows/:workflowId/resume - Agent —
POST /api/agents/:agentId/resume-stream
- Workflow —
- Reject maps to
resumeData.approved: false. Workflows shouldbail(); agent tools should return a rejection result to the model.
See the runnable project in examples/mastra (agent + workflow).

Setup
Create a workspace, API key, and Mastra connection (base URL + token).
Install @hitly/plugin-mastra:
yarn add @hitly/plugin-mastranpm i @hitly/plugin-mastrapnpm add @hitly/plugin-mastrabun add @hitly/plugin-mastraSet env:
HITLY_API_URL=http://localhost:3001
HITLY_API_KEY=hitly_...
HITLY_PROJECT_ID=prj_...
HITLY_RESUME_SECRET=hitly_resume_...
MASTRA_BASE_URL=http://localhost:4111Integration guide
- Scaffold a Mastra project (
yarn create mastra) or openexamples/mastra. - In the tool or step
execute, callnotifyHitlyApproval(), thensuspend(). - On resume, call
verifyHitlyResume(resumeData)then readresumeData.approved. Iffalse,bail()(workflow) or return a rejection (agent tool). Mastra Zod strips unknown keys —resumeSchemamust.passthrough()so the Hitlyhitlyproof survives. - Run
mastra dev(defaulthttp://localhost:4111) and Hitly app (http://localhost:3001). - Chat with the agent or start the workflow in Studio. Open
app.hitly.net/inbox, accept, watch the run resume.
Workflow step
import { createStep, createWorkflow } from '@mastra/core/workflows'
import { z } from 'zod'
import { notifyHitlyApproval, verifyHitlyResume } from '@hitly/plugin-mastra'
const approval = createStep({
id: 'hitly-approval',
inputSchema: z.object({ orderId: z.string(), amount: z.number() }),
outputSchema: z.object({ approved: z.boolean() }),
resumeSchema: z.object({ approved: z.boolean() }).passthrough(),
suspendSchema: z.object({ reason: z.string() }),
execute: async ({ inputData, resumeData, suspend, bail, runId }) => {
if (resumeData) verifyHitlyResume(resumeData, { runId, required: true })
if (resumeData?.approved === false) {
return bail({ reason: 'Reviewer rejected the refund.' })
}
if (!resumeData?.approved) {
await notifyHitlyApproval(
{
apiUrl: process.env.HITLY_API_URL!,
apiKey: process.env.HITLY_API_KEY!,
projectId: process.env.HITLY_PROJECT_ID!,
mastraBaseUrl: process.env.MASTRA_BASE_URL!,
workflowId: 'refund-workflow',
action: { name: 'send-refund', args: inputData },
},
{
runId: String(runId),
suspendPayload: { reason: `Refund ${inputData.amount} for order ${inputData.orderId}` },
},
)
return await suspend({ reason: 'Human approval required.' })
}
return { approved: true }
},
})Agent tool
Do not set requireApproval: true if Hitly should see the request. That flag pauses before execute, so notifyHitlyApproval never runs. Suspend inside the tool instead:
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'
import { notifyHitlyApproval, verifyHitlyResume } from '@hitly/plugin-mastra'
export const sendRefundTool = createTool({
id: 'send-refund',
description: 'Issue a customer refund after Hitly approval.',
inputSchema: z.object({ orderId: z.string(), amount: z.number() }),
resumeSchema: z.object({ approved: z.boolean() }).passthrough(),
suspendSchema: z.object({ reason: z.string() }),
execute: async (inputData, context) => {
const runId = (context as { runId?: string }).runId
const resumeData = context.agent?.resumeData as { approved?: boolean } | undefined
if (resumeData) verifyHitlyResume(resumeData, { runId, required: true })
if (resumeData?.approved === false) {
return { sent: false, message: 'Reviewer rejected the refund.' }
}
if (!resumeData?.approved) {
await notifyHitlyApproval(
{
apiUrl: process.env.HITLY_API_URL!,
apiKey: process.env.HITLY_API_KEY!,
projectId: process.env.HITLY_PROJECT_ID!,
mastraBaseUrl: process.env.MASTRA_BASE_URL!,
kind: 'agent',
agentId: 'refund-agent',
toolCallId: context.agent?.toolCallId,
action: { name: 'send-refund', args: inputData },
},
{ runId: String(runId), suspendPayload: { reason: `Refund ${inputData.amount}` } },
)
await context.agent?.suspend?.({ reason: 'Human approval required.' })
return { sent: false, message: 'Waiting for Hitly approval.' }
}
return { sent: true, message: 'Refund issued.' }
},
})Running locally
yarn workspace @hitly/app dev— Hitly on port 3001.yarn workspace @hitly/example-mastra dev(oryarn dev:mastra) — Studio on 4111.- Chat with Refund Agent or run refund-workflow. Approve in the inbox.
Production
Hitly must reach mastraBaseUrl to call resume(). Do not point a hosted Hitly workspace at localhost. Restrict Mastra CORS and use a token with workflow/agent resume access.
Configuration options
| Option | Use it to |
|---|---|
apiUrl | Hitly app origin, such as https://app.hitly.net |
apiKey | Workspace ingest key |
projectId | Project that owns the API key |
mastraBaseUrl | Origin Mastra server Hitly will call on resume |
kind | workflow (default) or agent |
workflowId / stepId | Workflow resume target |
agentId / toolCallId | Agent resume target |
Decision mapping
| Hitly | Mastra |
|---|---|
accept | resume({ resumeData: { approved: true } }) |
edit | resume({ resumeData: { approved: true, ...editedArgs } }) |
reject | resume({ resumeData: { approved: false } }) then bail() (workflow) or a rejection tool result (agent) |
respond | resume({ resumeData: { approved: true, response } }) |
ignore | no resume (approval closed) |
cancel | no resume (Force cancel — origin is no longer suspended) |