The Universal Harness API
The Universal Harness API is a managed agents API for every harness. One call describes an agent — any harness, any configuration — and starts it running in an isolated sandbox. No provisioning, no setup step: your first agent is running in a few seconds.
Every coding agent harness has its own installation, configuration format, authentication, and runtime quirks. Running one in production is real work; running several means maintaining several integrations. This API abstracts all of that behind one endpoint: the same payload, the same response shape, and the same thread endpoints for every harness.
$BRAINBASE_API_KEY. See Authentication for details.Quickstart
/v2/threadsDescribe the agent inline and pass its first input. Brainbase creates the agent, boots a sandbox, and starts the first turn — one request, nothing to set up beforehand.
agentobjectoptionalThe whole agent, inline: harness, instructions, model, tools. See the whole agent in one payload for every field.
agent_iduuidoptionalAlready have an agent? Pass its id instead of agent and skip the spec entirely.
inputstringoptionalThe first user message. The turn starts immediately.
The body also accepts title, metadata, group_id, and a messages array in place of input — see the full Create Thread reference.
The response is a handle to a running thread. Threads title themselves from the conversation, so you don't need to name anything.
curl https://api.brainbaselabs.com/v2/threads \
-H "Authorization: Bearer $BRAINBASE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": {
"harness": "claude_code",
"instructions": "You are a concise research assistant. Answer briefly and precisely.",
"model": "claude-sonnet-5"
},
"input": "In two sentences, what is the Model Context Protocol (MCP)?"
}'
{
"thread_id": "a4fe9c41-7fea-4b93-91e4-081329e35865",
"agent_id": "54850f1a-e68f-46ac-a85c-01f977101f91",
"status": "running"
}
Any harness
Change one line and the same request runs a different harness. Same payload, same response, same thread endpoints — the model, instructions, and tools carry over unchanged.
"agent": {
"harness": "codex",
"instructions": "You are a concise research assistant. Answer briefly and precisely.",
"model": "claude-sonnet-5"
}
Eight harnesses are supported today. The harness values below come straight from the live runtime registry — the same source that generates the accepted values in the API reference.
| Harness | Harness value |
|---|---|
| Claude Code | claude_code |
| Codex | codex |
| Cursor | cursor |
| Factory Droid | factory |
| Kafka — Brainbase's flagship runtime for knowledge work | kafka_cloud |
| OpenCode | opencode |
| Qoder | qoder |
| Qwen Code | qwen |
The whole agent in one payload
If you've used a serverless inference API, the shape is familiar: pass a spec, get a handle. The difference is what the spec describes. Instead of a model, you pass an agent — a harness, instructions, and whatever tools it needs, all inline.
"agent": {
"harness": "claude_code",
"instructions": "You are a research assistant.",
"model": "claude-opus-4-8",
"mcp_servers": [{ "name": "search", "url": "https://mcp.example.com/sse" }],
"skills": [{ "source": "registry:brainbase/deep-research" }],
"secrets": { "TAVILY_API_KEY": "tvly-..." },
"entrypoint": "pip install -r requirements.txt"
}
harnessstringoptionalWhich harness executes the agent's turns. Defaults to claude_code. See Any harness for the accepted values.
instructionsstringoptionalThe agent's system instructions — who it is and how it should work.
modelstringoptionalModel the harness runs, e.g. claude-sonnet-5 or claude-opus-4-8. Omit to use the harness default.
mcp_serversarrayoptionalMCP servers the agent can call. Each entry names a server and points at a url (remote) or command (local to the sandbox).
skillsarrayoptionalSkill declarations from the Brainbase registry, as registry:creator/slug with an optional @version pin.
secretsobjectoptionalKey–value secrets planted into the sandbox as plain environment variables, available to the harness and to your entrypoint.
entrypointstringoptionalBash that runs inside the sandbox before the agent launches — install dependencies, clone a repo, whatever your agent needs waiting for it when it wakes up. Runs with cwd=/workspace; failures are logged but don't block the agent from starting.
machine_kindstringoptionalSandbox provider the agent runs on. See Sandboxes.
titlestringoptionalOptional display title for the agent. Omit it and one is derived for you.
Same spec, same agent
Send the same agent spec twice and you get the same agent_id back, with a new thread each time. Brainbase hashes the spec and reuses the match, so you aren't minting a new agent on every call — describe the agent inline on every request and let the API dedupe. Change anything in the spec and you get a new agent.
// First call
{ "thread_id": "a4fe9c41-7fea-4b93-91e4-081329e35865",
"agent_id": "54850f1a-e68f-46ac-a85c-01f977101f91", "status": "running" }
// Identical spec, called again — same agent, new thread
{ "thread_id": "194c86d9-6b57-4128-83b8-a31fb8a7370e",
"agent_id": "54850f1a-e68f-46ac-a85c-01f977101f91", "status": "running" }
Already have an agent? Pass agent_id directly and skip the spec entirely.
{
"agent_id": "54850f1a-e68f-46ac-a85c-01f977101f91",
"input": "Same agent, new conversation."
}
Watch it run
A thread reports running while a turn is executing, then settles: success or fail for a finished turn, need_more_info when the agent is waiting on your answer, or idle when the turn ended without reporting an outcome. Watch it however you like.
/v2/threads/{thread_id}Poll the thread's status.{
"id": "a4fe9c41-7fea-4b93-91e4-081329e35865",
"agent_id": "54850f1a-e68f-46ac-a85c-01f977101f91",
"title": "Explain Model Context Protocol",
"status": "success",
"created_at": "2026-07-15T19:07:43.910211"
}
/v2/threads/{thread_id}/messagesFetch the transcript. Messages arrive in an `items` array.{
"items": [
{
"role": "user",
"content": "In two sentences, what is the Model Context Protocol (MCP)?"
},
{
"role": "assistant",
"content": "The Model Context Protocol (MCP) is an open standard (introduced by Anthropic) that defines how AI applications connect to external data sources, tools, and systems through a uniform client-server interface. ...",
"metadata": { "model": "claude-sonnet-5" }
}
]
}
/v2/threads/{thread_id}/events/streamA live server-sent events feed of everything the agent does.The stream carries the full activity of the thread as typed events — user.message, assistant.message.chunk (streaming text), assistant.message, tool_call.start, mcp.status, and idle (the turn's outcome and a summary) are the ones you'll see most, with comment keepalives in between. The connection stays open across turns, so follow-up messages and their replies arrive on the same stream.
curl -N "https://api.brainbaselabs.com/v2/threads/$THREAD_ID/events/stream" \
-H "Authorization: Bearer $BRAINBASE_API_KEY"
event: assistant.message.chunk
data: {"type":"assistant.message.chunk","thread_id":"a4fe9c41-…","data":{"content":[{"type":"text","content":"The Model Context Protocol (MCP) is an open standard…"}]}}
event: idle
data: {"type":"idle","thread_id":"a4fe9c41-…","data":{"status":"success","summary":"Answered a factual question about MCP in two sentences; no tools needed."}}
/v2/threads/{thread_id}/eventsThe same events as a paginated history (an `items` array) instead of a live stream./v2/tasks/{task_id}/interruptStop current work and cancel pending inputs. Read the current `generation` from `GET /v2/tasks/{task_id}/inputs` and send it as `expected_generation`; preserve that value and endpoint on retries. See [Stop and recovery](/api/tasks/interrupt-task) for partial cancellation and error handling.Keep the conversation going
/v2/tasks/{task_id}/inputsSend a follow-up to the existing conversation using its thread_id as the task_id. When the active harness supports native steering, the input can join the current turn. Otherwise it stays queued for a later turn; an idle task starts a new turn with the conversation context.
input_idstring · uuidrequiredA new UUID for each deliberate follow-up. Keep the same UUID and payload when retrying that follow-up.
messagesarrayrequiredOne or more user messages. Each needs content; role must be user when supplied.
expected_generationintegeroptionalRead generation from GET /v2/tasks/{task_id}/inputs before submitting and preserve it on retries. This fences the input against a concurrent Stop. Omitting it uses the current generation when the input is admitted.
HTTP 201 acknowledges a durable receipt, not native delivery or a completed answer. Poll GET /v2/tasks/{task_id}/inputs/{input_id} for its status. queued means it is waiting; submitted means it was assigned to a turn, which may still be starting. This does not confirm that the runtime consumed the input. Replies still arrive in the transcript and event stream.
After a lost response, retry the same input_id, messages, and model_options; Idempotency-Replayed: true identifies an existing receipt without resending it. An unconfirmed receipt means native delivery may have occurred. Keep that receipt; using a new ID can duplicate the instruction. See Follow-up retries and errors.
After a control-plane restart, an expired lease can still represent a turn waiting to be adopted. During the first 180 seconds after expiry, the API returns HTTP 409 with task is busy (active run lease); wait for it to finish, without a Retry-After header or a new input receipt. Retry the same input_id after the earlier turn is adopted or settles. Native steering is still accepted while the original or adopted lease is live. If adoption has not succeeded by 180 seconds, normal admission resumes; a still-running sandbox can then reject the new turn, and the task error identifies the earlier active_run_id and a control-plane restart as the usual cause.
For a task created from Linear, an API follow-up continues the same task but does not automatically post the prompt or reply to Linear. Reply in the Linear session when the conversation should remain visible there. See Linear conversations.
# Replace input_id with a new UUID for this follow-up; keep it on retries.
# Use the generation from GET /v2/tasks/$THREAD_ID/inputs (0 here).
curl "https://api.brainbaselabs.com/v2/tasks/$THREAD_ID/inputs" \
-H "Authorization: Bearer $BRAINBASE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input_id": "11111111-1111-4111-8111-111111111111",
"messages": [
{ "role": "user", "content": "Now name three popular MCP servers, one line each." }
],
"expected_generation": 0
}'
{
"input": {
"id": "11111111-1111-4111-8111-111111111111",
"task_id": "a4fe9c41-7fea-4b93-91e4-081329e35865",
"message_ids": ["bc85731e-7966-441f-ab57-3c92070e1438"],
"status": "queued",
"turn_id": null,
"generation": 0,
"sequence": 1,
"created_at": "2026-09-18T12:00:00Z",
"updated_at": "2026-09-18T12:00:00Z"
},
"messages": [
{
"id": "bc85731e-7966-441f-ab57-3c92070e1438",
"task_id": "a4fe9c41-7fea-4b93-91e4-081329e35865",
"role": "user",
"content": "Now name three popular MCP servers, one line each.",
"created_at": "2026-09-18T12:00:00Z"
}
],
"run_started": false,
"accepted_turn_id": null
}
{
"items": [
{
"role": "assistant",
"content": "- **Filesystem** — read/write access to local files and directories for an AI client.\n- **GitHub** — manage repos, issues, and PRs directly from an MCP-enabled assistant.\n- **Slack** — send messages and read channel history through Slack's API via MCP."
}
]
}
Sandboxes
Every agent runs in an isolated sandbox, and you choose the ground it runs on with machine_kind in the agent spec. The provider is fixed when the agent is created, and which ones your deployment offers depends on how it is configured.
| Provider | Sizes | Runtimes | Shared folders | Notes |
|---|---|---|---|---|
daytona | S, M, L | All | Yes | The default. |
e2b | L | All | No | |
cloudflare | S, M, L | Coding agents only | Yes | Sleeps when idle and resumes with its workspace intact. |
modal | L | Coding agents only | No | Sandboxes are short-lived: 24 hours maximum, and a task does not resume afterwards. |
bbsandbox | L | Coding agents only | No | Self-hosted. |
aws_microvm | L | Coding agents only | No |
claude_code, codex, qwen, opencode, cursor, factory and qoder. Those four providers refuse every other runtime_kind, including the Kafka harness, and the refusal happens at agent create — pairing one of them with kafka_cloud fails rather than falling back. The capabilities endpoint below does not report this, so the table is the only source: a picker that offers a runtime a provider cannot run will have it refused.GET /v2/machines/capabilities reports the shape of each provider — the sizes it accepts, whether it supports shared folders, which accelerators it offers — so a client can render a picker without hard-coding most of the table above. Supported runtimes are the exception: they are not in the response, so that column has to be carried by the client.
503 if it is switched off, or 403 if your account is outside its rollout. Treat the response as the shape of a provider, and the create call as the only authority on whether you may use it."agent": {
"harness": "claude_code",
"instructions": "You are a research assistant.",
"machine_kind": "daytona"
}
See it on the platform
Everything you create through the API also shows up on the Brainbase platform — open app.brainbaselabs.com and the agent and its threads are right there, with the full conversation streaming live.