Agentic Workforce ME Developer PortalDocs 1.1 · Widget 0.1.0

Backend integration

Start runs from your backend

Start an agent run or a thread from your server, stream SSE or poll, attribute the end user — with @hive/sdk, curl, Node, Python, .NET and Java.

The most direct way to put an Agentic Workforce ME agent to work from your backend is to start a run. There are two shapes: a thread (a conversation that keeps context across turns — one per case, ticket or customer) and a one-off run (no thread; the input is the whole job). Either way you get a run_id back immediately (202) and follow it by streaming or polling. For event-driven starts without an API key in the caller, use a trigger instead.

Thread and message (conversation)

  1. POST /v1/threads with the agent_id of a published agent.
  2. POST /v1/threads/:id/messages with content. The platform stores the message, creates one governed run and returns { run_id, message_id }. Send an Idempotency-Key header (any string unique to the attempt, such as your ticket id plus a turn counter) so a retried request replays the first response instead of creating a second run.
  3. Follow the run (below). Post the next turn on the same thread when the user replies.
conversation.tsTypeScript
import { hive } from './hive';

// 1. A thread keeps context across turns (one per customer conversation / case).
const thread = await hive.threads.create({ agent_id: AGENT_ID, title: 'Order ord_1001' });

// 2. Post a turn → the platform creates one governed run (202). Idempotency-Key makes retries safe.
const { run_id } = await hive.threads.postMessage(
  thread.id,
  { content: 'Customer reports the item arrived damaged. Order ord_1001. What are the options?' },
  { idempotencyKey: `ticket-4821-turn-1` },
);

// 3a. Stream: persisted backfill first, then live until terminal.
//     RunEvent is { type: string; [key: string]: unknown } — narrow the fields you use.
for await (const event of hive.runs.events(run_id)) {
  switch (event.type) {
    case 'llm.delta':      process.stdout.write(String(event.text)); break;
    case 'tool.called':    console.log('\n→ tool', event.tool, event.args_preview); break;
    case 'approval.required':
      console.log('\n⏸ waiting for approval', event.approval_id); break;
    case 'run.completed':  console.log('\n✓ done', event.cost_usd, 'USD'); break;
    case 'run.failed':     console.error('\n✗', event.code, event.message); break;
  }
}

// 3b. …or just wait for the final row.
const run = await hive.runs.waitForCompletion(run_id);
console.log(run.status, run.output);

Per-turn options let a caller tighten the agent for one run: autonomy: "require_approval" turns every automatic tool call into an approval request; evidence: true requires grounded answers; tools: [...] narrows the tool set to a subset of what is granted. None of them can widen what the published manifest allows.

One-off run

POST /v1/agents/:id/runs starts a run with no thread. The body is { input }; the agent reads its prompt from a bare string, { text } or { input }, and the whole input object is available to the run. The run’s origin records your API key and it is audited as run.manual.

TypeScript
// No thread: a one-off run. POST /v1/agents/:id/runs is REST-only today — the
// SDK's request helper is private, so use fetch for this one call, then the SDK for the rest.
const res = await fetch(`${process.env.HIVE_API_URL}/v1/agents/${AGENT_ID}/runs`, {
  method: 'POST',
  headers: {
    authorization: `Bearer ${process.env.HIVE_API_KEY}`,
    'x-tenant-id': process.env.HIVE_TENANT_ID!,
    'content-type': 'application/json',
  },
  body: JSON.stringify({ input: { text: 'Summarise the refund policy for damaged items in two sentences.' } }),
});
const { run_id } = (await res.json()) as { run_id: string };   // 202
const run = await hive.runs.waitForCompletion(run_id);

Follow the run

Stream with Server-Sent Events

GET /v1/runs/:id/events with Accept: text/event-stream replays the persisted steps after ?since= (default -1, everything) as step.started frames — plus llm.thinking where the model emitted any — then stays open until the run is terminal. Tool results and approval requests are not replayed: a reconnecting client reads GET /v1/runs/:id (and GET /v1/approvals?run_id=) for the current tool and approval state. Each frame is event: <type> plus data: <json>; the JSON also carries type. A heartbeat arrives every 15 s. Reconnect with the last idx you processed to resume without duplicates.

HTTP
event: run.started
data: {"type":"run.started","run_id":"019…","agent_id":"019…","status":"running"}

event: step.started
data: {"type":"step.started","idx":0,"kind":"llm","name":"claude-sonnet-4-5"}

event: llm.delta
data: {"type":"llm.delta","idx":0,"text":"Checking the order"}

event: tool.called
data: {"type":"tool.called","idx":1,"tool":"orders.get","args_preview":{"order_id":"ord_1001"}}

event: tool.result
data: {"type":"tool.result","idx":1,"ok":true,"output_preview":{"status":"delivered","total_minor":12900}}

event: heartbeat
data: {"type":"heartbeat"}

event: run.completed
data: {"type":"run.completed","status":"succeeded","output":{"text":"…"},"tokens_in":812,"tokens_out":140,"cost_usd":0.0031}
typedataDescription
run.started{ run_id, agent_id, status }The worker picked the run up.
step.started{ idx, kind, name }A persisted step began (llm, tool, retrieval, approval…). idx is the resume cursor.
llm.delta{ idx, text }Streamed assistant text.
llm.thinking{ idx, text, replace? }Model reasoning channel when the model exposes one.
tool.called{ idx, tool, args_preview }A tool call started; arguments are truncated and redacted.
tool.result{ idx, ok, output_preview }The tool returned (or failed).
retrieval.result{ idx, kb, chunks[] }Knowledge-base hits with scores.
approval.required{ approval_id, tool, expires_at }The run paused on a human gate — also delivered as the approval.requested webhook.
run.waiting{ status: "waiting_approval" }Status transition to waiting.
run.resumed{ approval_id, decision }A terminal decision resumed the run.
run.completed{ status, output, tokens_in, tokens_out, cost_usd }Terminal; the server closes the stream.
run.failed{ code, message }Terminal failure (includes CANCELLED).
node.started{ node_id, node_type }Workflow runs: a node began.
node.completed{ node_id, node_type, status, child_run_id? }Workflow runs: a node ended (succeeded | failed | skipped | waiting).
handoff{ from, to }Workflow runs: an edge was traversed.
heartbeat{}Every 15 s while the stream is open.

Or poll

GET /v1/runs/:id returns { run, steps }. Poll once a second until run.status is terminal; the response includes the final output, aggregated tokens and cost, and every step with redacted input/output. Better still: subscribe to the run.completed / run.failed webhooks and stop polling altogether.

StatusMeaning
queuedAccepted (202) and waiting for a worker.
runningThe graph is executing.
waiting_approvalPaused on a human gate; resumes on a terminal decision.
succeededTerminal — run.completed webhook.
failedTerminal — run.failed webhook (also when cancelled while waiting).
cancelledTerminal — POST /v1/runs/:id/cancel, or an approval SLA elapsed under on_timeout: cancel_run (SSE run.failed with code EXPIRED).
expiredReserved in the status enum; the runtime does not currently set it (approvals expire, runs are cancelled).

Passing context and attributing the end user

  • Structured context goes in the input. For a one-off run, put your record under the same object as text ({ text, order: {...}, customer_id }); for a thread, include it in the message content or attach documents with attachment_ids.
  • Correlation. Store the returned run_id on your record (the example writes it on the order). Webhooks and GET /v1/runs/:id carry the same id, so you never need to search.
  • End users. Runs expose an end_user_id, but it is set by the channels that authenticate a person — the widget’s identity JWT, WhatsApp, email — not by POST /v1/threads, which has no such field today. When your backend acts on behalf of a customer, carry your customer id in the thread title and the input, and correlate on your side. Tenant end-user records (/v1/end-users, unique external_id) remain the place to keep PII tags and residency for that person.
  • Attribution on the platform. Runs started with a key show apikey:<id> as the actor; runs started by a trigger record the trigger. Use one key per service so the audit trail names the service.

SDKs

@hive/sdk (TypeScript, Node ≥ 18) wraps the surfaces below; request types are the API’s Zod-inferred DTOs, and non-2xx responses throw HiveApiError with status, code and detail. The Python client (hive-sdk, in sdks/python) mirrors the same calls in snake_case.

conversation.pyPython
from hive_sdk import HiveClient

hive = HiveClient(base_url=HIVE, api_key=API_KEY, tenant_id=TENANT_ID)
thread = hive.threads.create(agent_id=AGENT_ID, title="Order ord_1001")
accepted = hive.threads.post_message(thread["id"], "Item arrived damaged — options?",
                                     idempotency_key="ticket-4821-turn-1")
run = hive.runs.wait_for_completion(accepted["run_id"])
print(run["status"], run["output"])

@hive/sdk methods used from a backend

MethodCallsReturns
agents.list(options?)GET /v1/agentsPage<Agent>
agents.get(id)GET /v1/agents/:id{ agent }
agents.create(input)POST /v1/agentscreated agent
agents.publishVersion(id, version)POST /v1/agents/:id/versions/:version/publishpublished version
threads.list(options?)GET /v1/threadsPage<Thread>
threads.create({ agent_id, title? })POST /v1/threadsThread
threads.messages(threadId)GET /v1/threads/:threadId/messages{ items }
threads.postMessage(threadId, { content }, { idempotencyKey? })POST /v1/threads/:threadId/messages{ run_id, message_id }
runs.list({ agent_id?, workflow_id?, status? })GET /v1/runsPage<Run>
runs.get(id)GET /v1/runs/:id{ run, steps }
runs.cancel(id)POST /v1/runs/:id/cancel{ run_id, status }
runs.events(id, { since?, signal? })GET /v1/runs/:id/eventsAsyncGenerator<RunEvent>
runs.waitForCompletion(id)GET /v1/runs/:id/events then GET /v1/runs/:idRun
workflows.list(options?)GET /v1/workflowsPage
workflows.get(id)GET /v1/workflows/:idworkflow
workflows.testRun(id, { input, variables? })POST /v1/workflows/:id/test-run{ run_id }
approvals.list({ status?, run_id?, agent_id?, risk? })GET /v1/approvals{ items }
approvals.get(id)GET /v1/approvals/:idApproval
approvals.decide(id, { action, edited_args?, feedback? })POST /v1/approvals/:id/decisionApproval
webhooks.list()GET /v1/webhooks{ items }
webhooks.create({ url, events, description? })POST /v1/webhooksendpoint + secret (once)
webhooks.update(id, patch)PATCH /v1/webhooks/:idendpoint
webhooks.delete(id)DELETE /v1/webhooks/:idvoid
webhooks.rotateSecret(id)POST /v1/webhooks/:id/rotate-secretendpoint + secret (once)
webhooks.deliveries(id, options?)GET /v1/webhooks/:id/deliveriesPage<WebhookDelivery>
webhooks.test(id)POST /v1/webhooks/:id/testaccepted

Not wrapped yet — call these with fetch (the demo portals do): /v1/triggers (+ /hooks/:triggerId), /v1/mcp-servers, /v1/connections, /v1/tool-grants, /v1/approval-policies, /v1/api-keys, POST /v1/agents/:id/runs, POST /v1/workflows/runs/:runId/events.

hive-sdk (Python)

MethodCallsReturns
hive.agents.list()GET /v1/agentsdict page
hive.threads.create(agent_id)POST /v1/threadsdict
hive.threads.post_message(thread_id, content)POST /v1/threads/:id/messages{ run_id, message_id }
hive.runs.events(run_id)GET /v1/runs/:id/eventsiterator of events
hive.runs.wait_for_completion(run_id)SSE then GET /v1/runs/:idrun dict
hive.approvals.list(status="pending")GET /v1/approvals{ items }
hive.approvals.decide(id, "approve")POST /v1/approvals/:id/decisiondict
hive.webhooks.create(url, events)POST /v1/webhooksendpoint + secret (once)
verify_webhook_signature(secret, header, raw_body)WebhookVerification(ok, reason, timestamp)

Endpoints

POST/v1/threadsOpen a conversation thread on one agent.
Auth
API key + X-Tenant-Id (any member; an API key acts as admin)
Errors
UNAUTHORIZED TENANT_REQUIRED VALIDATION NOT_FOUND

Request

FieldTypeDescription
agent_iduuidA published agent.
titlestring?Optional title (1–200).

Response

FieldTypeDescription
iduuidThread id.
POST/v1/threads/:id/messages202Post a user message; the platform creates and enqueues one governed run.
Auth
API key + X-Tenant-Id (any member; an API key acts as admin)
Errors
UNAUTHORIZED TENANT_REQUIRED VALIDATION NOT_FOUND

Request

FieldTypeDescription
contentstringThe user turn (up to 32 000 chars). Text and/or attachments are required.
attachment_idsuuid[]?Up to 8 uploaded attachments (vision).
autonomy`require_approval`?Tighten-only: every auto HITL resolution becomes require_approval for this run. Nothing can loosen the manifest.
evidencetrue?Grounded-only answer for this run (knowledge-base citations required).
deliverable`xlsx` | `pptx` | `html`?Steer this run to produce a document deliverable (agent must have documents enabled).
toolsstring[]?Per-turn allow-list of tool tokens the run may use. Only narrows the granted set; an unknown token is rejected (400 UNKNOWN_TOOL).

Response

FieldTypeDescription
run_iduuidFollow it with SSE or polling.
message_iduuidThe stored user message.
  • Send an Idempotency-Key header to make a double-submit replay the first response instead of creating a second run.
POST/v1/agents/:id/runs202Run an agent once with no thread (trigger: manual).
Auth
API key + X-Tenant-Id (any member; an API key acts as admin)
Errors
UNAUTHORIZED TENANT_REQUIRED NOT_FOUND AGENT_NOT_PUBLISHED

Request

FieldTypeDescription
inputunknown?Run input. The agent reads its prompt from a bare string, { text } or { input }; any other shape leaves the model with only its system prompt.

Response

FieldTypeDescription
run_iduuidThe queued run.
  • Audited as run.manual; the run’s origin records your API key.
GET/v1/runsList runs, newest first.
Auth
API key + X-Tenant-Id (any member; an API key acts as admin)
Errors
UNAUTHORIZED TENANT_REQUIRED VALIDATION

Request

FieldTypeDescription
agent_iduuid?Filter by agent.
workflow_iduuid?Filter by workflow.
statusRunStatus?queued running waiting_approval succeeded failed cancelled expired.
cursor / limitstring? / number?Keyset pagination (default 25, max 100).
GET/v1/runs/:idOne run with its steps — the terminal state, output, cost and tool calls.
Auth
API key + X-Tenant-Id (any member; an API key acts as admin)
Errors
UNAUTHORIZED TENANT_REQUIRED NOT_FOUND

Response

FieldTypeDescription
run.statusRunStatusSee the run state machine.
run.outputunknownAgent runs: { text, json? }. Workflow runs: { outputs: { <node_id>: { text, child_run_id } }, variables }.
run.originobjectProvenance: trigger, API key, user, channel or embed key that started it.
steps[]RunStep[]llm, tool, approval, retrieval… with redacted input/output.
GET/v1/runs/:id/eventsServer-Sent Events: persisted backfill from ?since=, then live until terminal.
Auth
API key + X-Tenant-Id (any member; an API key acts as admin)
Errors
UNAUTHORIZED TENANT_REQUIRED NOT_FOUND

Request

FieldTypeDescription
sincenumber?Last idx you have seen; -1 (default) replays every persisted step first.
  • Send Accept: text/event-stream. Each message is event: <type> + data: <json>; a heartbeat arrives every 15 s.
  • The full vocabulary (tool names, arguments, retrieval hits, node events) is visible here because the caller is a trusted tenant principal.
POST/v1/runs/:id/cancelBest-effort cancel; pending approvals of the run are cancelled too.
Auth
API key + X-Tenant-Id (any member; an API key acts as admin)
Errors
UNAUTHORIZED TENANT_REQUIRED NOT_FOUND