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.
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.failedwebhooks 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 (also when cancelled while waiting).
cancelled
Terminal — POST /v1/runs/:id/cancel, or an approval SLA elapsed under on_timeout: cancel_run (SSE run.failed with code EXPIRED).
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.
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.
@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.