HUMANin theLOOP

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

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

Install @hitly/plugin-mastra:

yarn add @hitly/plugin-mastra
npm i @hitly/plugin-mastra
pnpm add @hitly/plugin-mastra
bun add @hitly/plugin-mastra

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

  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 https://app.hitly.net
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

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