The headless agent endpoint drives the full agent loop once over an ephemeral, gateway-owned session. It accepts a prompt (and optional workflow, model, output format, and timeout) and returns the result either as an async job to poll or as a Server-Sent Events stream. This route is intended for one-shot automation, CI pipelines, and integration scripts that need a single autonomous run without interactive approvals.
Drives the full agent loop once over an ephemeral gateway-owned session and returns text, json, or stream-json. The default (json / text) form is an async job: the gateway returns 202 with a job_id and the caller polls GET /api/v1/agent/jobs/{id} / GET /api/v1/agent/jobs/{id}/result for the captured Result. Setting format: stream-json (or stream: true) instead streams the run over SSE, emitting start, result / error, and end frames.
Per-request working-directory scope: the .hoody project layer, record cwd, and tool / workflow cwd. Required by routes that resolve a cwd (for example, POST /todos; todos.create also accepts a body cwd).
X-Hoody-Config-Dir
header
string
No
Per-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves (HoodyPaths).
X-Hoody-Container
header
string
No
Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension.
X-Hoody-Realm
header
string
No
Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes.
realm
query
string
No
Per-request realm selector, the in: query alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes.
The prompt to drive the ephemeral session. Required unless workflow is set; with a workflow it is the optional $(workflow.prompt) text.
workflow
string
No
Optional workflow name to run instead of a plain prompt turn.
inputs
object
No
Optional run-time values for the workflow’s declared input parameters (declared name → string value; resolves to $(input.<name>) in every step). Only accepted with workflow. A workflow with a required declared parameter cannot run without these. Values must be strings; a non-string value is a 400.
model
string
No
Optional model spec for the run.
format
string
No
Output rendering: text | json | stream-json. stream-json (or stream: true) streams the run over SSE; otherwise the run is an async job.
stream
boolean
No
Force SSE streaming (equivalent to format: stream-json).
timeout_ms
integer
No
Optional run timeout in milliseconds (clamped to the hard ceiling).
{
"prompt":"Refactor src/parser.ts to use the new lexer and report a diff summary.",
data: {"text":"Refactored 14 statements; 1 statement left on disk.\n","tokens":{"input":1284,"output":412}}
event: end
data: {"status":"ok"}
Run accepted as an async job (format: text | json). The run executes after this ack; its outcome, including a server-side failure, is delivered via GET /api/v1/agent/jobs/{id}/result, not as an HTTP status on this op.
{
"job_id":"job_01HV8A2C3K7Q5R8X9Y1Z2A3B4C"
}
Poll with GET /api/v1/agent/jobs/{id} for status and GET /api/v1/agent/jobs/{id}/result for the captured Result, or for { "status": "failed", "error": { ... } } if the daemon failed. A run whose turn was cancelled settles with { "status": "cancelled", "outcome": "cancelled" } and keeps the partial output in result.
{
"code":"bad_request",
"message":"invalid request"
}
Error Code
Title
Description
Resolution
bad_request
Bad request
The request was malformed or carried invalid parameters.
Correct the request body or query parameters.
invalid_realm
Invalid realm selector
The realm selector is malformed (not ""/“global”/a 24-hex id).
Pass a valid realm selector.
realm_scope_unsupported
Realm scope unsupported
A per-request scope header this route does not take: a realm or container header on a route without that dimension, or, on an agent pinned to one realm, a different realm.
Omit the header, or name the realm the agent is pinned to.
{
"code":"forbidden",
"message":"request must arrive through the Hoody proxy"
}
Error Code
Title
Description
Resolution
forbidden
Forbidden (Source IP Guard)
The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only.
Call the agent through its URL, for example with hoody agent ….
{
"code":"payload_too_large",
"message":"request body exceeds the configured size limit"
}
Error Code
Title
Description
Resolution
payload_too_large
Payload too large
The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge (http.MaxBytesReader) before the handler reads it; a well-formed-but-large body is a size violation, not a JSON syntax error.
Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests.
{
"code":"rate_limited",
"message":"request rate limit exceeded"
}
Error Code
Title
Description
Resolution
rate_limited
Too many requests
The per-client request rate limit was exceeded; the gateway throttled the request before dispatch.
Honor the Retry-After header and retry; reduce the request rate.
{
"code":"internal_error",
"message":"internal server error"
}
Error Code
Title
Description
Resolution
internal_error
Internal error
An unexpected error occurred while handling the request.
Retry; if persistent, inspect the daemon logs.
{
"code":"service_unavailable",
"message":"service unavailable"
}
Error Code
Title
Description
Resolution
service_unavailable
Service unavailable
The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit).
The SDK exposes this operation through two named methods. They differ only in what they set on the body:
client.agent.headless.start(data, options?) — sets body.stream = false and limits the body format field to "text" | "json". Use this for the default async-job flow.
client.agent.headless.stream(data, options?) — sets body.stream = true. Use this to receive the run as an SSE stream.
"prompt": "Refactor src/parser.ts to use the new lexer and report a diff summary.",
"workflow": "refactor-and-summarize",
"inputs": {
"target_path": "src/parser.ts",
"summary_format": "diff"
},
"model": "claude-opus-4.5",
"format": "json",
"timeout_ms": 300000
}'
The response carries a job_id; poll GET /api/v1/agent/jobs/{id}/result for the captured Result. To switch to SSE, change "format": "json" to "format": "stream-json" (or add "stream": true) and consume the text/event-stream body.