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.
POST /v1/threads with the agent_id of a published agent.
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.
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.forawait(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.
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 =awaitfetch(`${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};// 202const run =await hive.runs.waitForCompletion(run_id);
// REST without the SDK: start, then consume SSE with fetch.const started =await hive<{run_id:string}>('POST',`/v1/agents/${AGENT_ID}/runs`,{input:{text:'Summarise the refund policy for damaged items.'},});const res =awaitfetch(`${process.env.HIVE_API_URL}/v1/runs/${started.run_id}/events?since=-1`,{headers:{accept:'text/event-stream',authorization:`Bearer ${process.env.HIVE_API_KEY}`,'x-tenant-id': process.env.HIVE_TENANT_ID!,},});const reader = res.body!.pipeThrough(newTextDecoderStream()).getReader();let buffer ='';for(;;){const{ value, done }=await reader.read();if(done)break;
buffer += value;letsep: number;while((sep = buffer.indexOf('\n\n'))!==-1){const frame = buffer.slice(0, sep);
buffer = buffer.slice(sep +2);const data = frame.split('\n').find((l)=> l.startsWith('data: '))?.slice(6);if(data ===undefined)continue;const event =JSON.parse(data)as{type:string;[k:string]: unknown };if(event.type ==='run.completed'|| event.type ==='run.failed') console.log(event);}}
Python
import json
# Start (one-off run), then follow the SSE stream with requests.
started =hive("POST", f"/v1/agents/{AGENT_ID}/runs",
json={"input":{"text":"Summarise the refund policy for damaged items."}})
run_id = started["run_id"]with session.get(HIVE+ f"/v1/runs/{run_id}/events", params={"since":-1},
headers={"Accept":"text/event-stream"}, stream=True, timeout=120)asres:for line in res.iter_lines(decode_unicode=True):ifnot line.startswith("data: "):
continue
event = json.loads(line[6:])if event["type"]=="llm.delta":print(event["text"], end="", flush=True)elif event["type"]in("run.completed","run.failed"):print("\n", event)
break
# Or poll: GET /v1/runs/{run_id} until status is terminal
run =hive("GET", f"/v1/runs/{run_id}")["run"]
C#
var start =await hive.PostAsJsonAsync($"/v1/agents/{agentId}/runs",new{ input =new{ text ="Summarise the refund policy for damaged items."}});var runId =(await start.Content.ReadFromJsonAsync<Dictionary<string,string>>())!["run_id"];// Poll until terminal (or open /v1/runs/{id}/events with Accept: text/event-stream)while(true){var run =await hive.GetFromJsonAsync<System.Text.Json.JsonElement>($"/v1/runs/{runId}");var status = run.GetProperty("run").GetProperty("status").GetString();if(status is"succeeded" or "failed" or "cancelled"){Console.WriteLine(run);break;}awaitTask.Delay(1000);}
Java
HttpRequest start =HttpRequest.newBuilder(URI.create(base +"/v1/agents/"+ agentId +"/runs")).header("Authorization","Bearer "+System.getenv("HIVE_API_KEY")).header("X-Tenant-Id",System.getenv("HIVE_TENANT_ID")).header("Content-Type","application/json").POST(HttpRequest.BodyPublishers.ofString("{\"input\":{\"text\":\"Summarise the refund policy for damaged items.\"}}")).build();String runId =newObjectMapper().readTree(http.send(start,HttpResponse.BodyHandlers.ofString()).body()).get("run_id").asText();// Poll GET /v1/runs/{id} until run.status is succeeded | failed | cancelled,// or stream GET /v1/runs/{id}/events with Accept: text/event-stream.
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.
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.progress
{ idx, tool, data }
Progress from a long-running built-in tool while it works — today the prototype page builder (data.typeprototype.page.estimate | prototype.page.delta | prototype.page.phase). In order, streamed page text coalesced, everything flushed before the call’s tool.result. Live only: it is not persisted, so a ?since= reconnect does not replay it. Not relayed to the widget.
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.
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 / run.cancelledwebhooks and stop polling altogether.
Status
Meaning
queued
Accepted (202) and waiting for a worker.
running
The graph is executing.
waiting_approval
Paused on a human gate; resumes on a terminal decision.
succeeded
Terminal — run.completed webhook.
failed
Terminal — run.failed webhook.
cancelled
Terminal — POST /v1/runs/:id/cancel (a running workflow stops at its next node/await point together with every child run it spawned), or an approval SLA elapsed under on_timeout: cancel_run. SSE run.failed with code CANCELLED; run.cancelled webhook. Never resumes.
expired
Reserved in the status enum; the runtime does not currently set it (approvals expire, runs are cancelled).
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.
Documents (vision). Upload PNG, JPEG, WebP or PDF files (≤ 20 MB each) with POST /v1/threads/:id/attachments, then pass the returned ids in the message’s attachment_ids (max 8). The upload answers with metadata only — including a PDF’s total page_count, so your brief can tell the agent how many pages each document has. At run time every image the model sees is preceded by a caption Attachment <n>: <filename> (<mime>, page <p> of <N>) — <n> is the position in your attachment_ids — so a vision agent can attribute each page to the right slot; name files meaningfully and echo the same filenames in your prompt. PDF pages are rasterized; a turn uses at most 8 images. The run’s llm step records the captions under input.vision, never the bytes.
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. The end user also decides what the agent remembers: long-term memory and the memory graph are scoped to the served contact, never shared between people — see Memory & the memory graph.
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.
@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.
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, /v1/memory/graph/*.
The user turn (up to 32 000 chars). Text and/or attachments are required.
attachment_ids
uuid[]?
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.
evidence
true?
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).
tools
string[]?
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
Field
Type
Description
run_id
uuid
Follow it with SSE or polling.
message_id
uuid
The stored user message.
Send an Idempotency-Key header to make a double-submit replay the first response instead of creating a second run.
Vision: every image the model receives is preceded by a text caption Attachment <n>: <filename> (<mime>[, page <p> of <total>]) — <n> is the attachment’s 1-based position in attachment_ids, one caption per rendered PDF page with the document’s true page total. Name your files meaningfully and the agent can attribute each page to the right document; the run’s llm step records the captions under input.vision (never the bytes).
POST/v1/threads/:id/attachments201Upload images or PDFs to a thread before the message that uses them (multipart/form-data).
Auth
API key + X-Tenant-Id (any member; an API key acts as admin)
Metadata only. kind is image or pdf; page_count is the PDF’s total page count, read at upload (null for images and for a PDF the parser cannot open — such a PDF is still accepted and skipped by vision later, it never fails the run). Pass each id in the next message’s attachment_ids (max 8).
Bytes are stored privately in the object store and never returned by the API. At run time images are sent to the vision model directly; PDF pages are rasterized (the first 8 images of a turn are used) — use page_count to tell the agent how many pages each document has.
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)
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/cancelCancel a run. Paused at a gate ⇒ cancelled immediately (pending approvals voided). Queued/running ⇒ the run and every child run it spawned (workflow agent nodes, parallel branches, sub-workflows) stop at their next check and end cancelled; one run.cancelled webhook. Already terminal ⇒ 409 BAD_STATE.
Auth
API key + X-Tenant-Id (any member; an API key acts as admin)