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

  1. An agent tool or workflow step calls suspend() and POSTs the payload to HITLy.
  2. HITLy stores an envelope and shows it in the inbox.
  3. A reviewer accepts, edits, or rejects.
  4. HITLy resumes the origin:
    • WorkflowPOST /api/workflows/:workflowId/resume
    • AgentPOST /api/agents/:agentId/resume-stream
  5. Reject maps to resumeData.approved: false. Workflows should bail(); agent tools should return a rejection result to the model.

See the runnable project in examples/mastra (agent + workflow).

send-refund Mastra work item with refund context, order data, and Accept decision
Refund workflow paused in HITLy

Setup

Clone the monorepo and set up the local HITLy app:

git clone https://github.com/hitly-net/hitly.git
cd hitly
yarn install
cp apps/app/.env.example apps/app/.env.local
yarn db:up
yarn db:migrate
yarn dev:app  # http://localhost:3001

Create a workspace, API key, and Mastra connection (base URL + token).

Set 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:4111

Integration guide

  1. Scaffold a Mastra project (yarn create mastra) or open examples/mastra.
  2. In the tool or step execute, call notifyHitlyApproval(), then suspend().
  3. On resume, call verifyHitlyResume(resumeData) then read resumeData.approved. If false, bail() (workflow) or return a rejection (agent tool). Mastra Zod strips unknown keys — resumeSchema must .passthrough() so the HITLy hitly proof survives.
  4. Run mastra dev (default http://localhost:4111) and HITLy app (http://localhost:3001).
  5. Chat with the agent or start the workflow in Studio. Open http://localhost:3001/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

  1. yarn workspace @hitly/app dev — HITLy on port 3001.
  2. yarn workspace @hitly/example-mastra dev (or yarn dev:mastra) — Studio on 4111.
  3. 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

OptionUse it to
apiUrlHITLy app origin, such as http://localhost:3001
apiKeyWorkspace ingest key
projectIdProject that owns the API key
mastraBaseUrlOrigin Mastra server HITLy will call on resume
kindworkflow (default) or agent
workflowId / stepIdWorkflow resume target
agentId / toolCallIdAgent resume target

Evidence Fields (Optional)

For audit and compliance, pass evidence context to notifyHitlyApproval():

FieldMastra FillsImplementor ProvidesDescription
traceIdFrom OTel contextIf no OTelDistributed trace ID
spanIdFrom OTel contextIf no OTelCurrent span ID
agentIdFor agentsFor workflowsAgent/workflow identifier
systemIdNoYesAI system being governed (e.g. "refund-agent-prod")
inventoryIdNoYesThis AI system's record in your inventory/registry (Layer 1)
policyIdNoYesPolicy that triggered approval
policyRationaleNoYesWhy approval is required
riskTierNoYesRisk level (e.g. "high", "medium", "low")
toolNameNoYesTool or action name
sensitivityNoYesSensitivity flags (not raw PII)
dataCategoriesNoYesData category flags (GDPR/compliance)

Example with evidence:

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!,
    agentId: 'refund-agent',
    action: { name: 'send-refund', args: inputData },

    // Evidence fields
    systemId: 'refund-agent-prod',
    inventoryId: 'ai-inv-refund-agent-v2',  // Your AI inventory record, not orderId
    policyId: 'refund-over-100',
    policyRationale: 'Refunds over $100 require manager approval',
    riskTier: 'medium',
    toolName: 'send_refund',
  },
  { runId: String(runId), suspendPayload: { reason: 'Approval required' } },
)

Evidence events are written to the configured HTTP sink (project settings → Evidence Storage). See Envelope for full details.

Decision mapping

HITLyMastra
acceptresume({ resumeData: { approved: true } })
editresume({ resumeData: { approved: true, ...editedArgs } })
rejectresume({ resumeData: { approved: false } }) then bail() (workflow) or a rejection tool result (agent)
respondresume({ resumeData: { approved: true, response } })
ignoreno resume (approval closed)
cancelno resume (Force cancel — origin is no longer suspended)

On this page