Skip to main content

HTTP Route Reference

harnessd exposes a REST + Server-Sent Events (SSE) API over a single TCP port (default :8080). Every agent run, subagent, scheduled job, script workflow, and relay worker is reachable through this surface. This page covers the primary public route inventory — method, scope, request/response shape, and notes on when a route requires an optional server component. Note: the definition-based workflow routes (/v1/workflows*, /v1/workflow-runs/*) are registered but not yet fully documented here.

Key terms used throughout this page:

  • Scope — the API key permission level required. Three levels exist: runs:read (read-only access), runs:write (mutations; also satisfies runs:read), and admin (superscope; satisfies any check). See Authentication.
  • 501 — a route returns 501 Not Implemented when the server option or backing store it depends on is not configured (for example, GET /v1/runs requires a persistent store, and relay routes require HARNESS_RELAY_DB).
  • SSE — Server-Sent Events. Streaming endpoints return Content-Type: text/event-stream and bypass the 30-second handler timeout.

Several routes in this table are only active when the corresponding server option is wired at startup. Routes marked "requires store" need HARNESS_RUN_DB to be set. Routes for skills, subagents, script-workflows, and relay workers return 501 when their respective managers are not configured.


Authentication

All routes (except /healthz and the webhook routes) pass through authMiddleware.

How to authenticate: pass Authorization: Bearer <token> on every request. SSE clients that cannot set request headers may use the ?token=<token> query parameter instead.

When auth is disabled: set HARNESS_AUTH_DISABLED=true, or start without HARNESS_RUN_DB — no store means no key validation, so auth is implicitly off.

ScopeConstantWhat it grants
runs:readstore.ScopeRunsReadGET on runs, conversations, models, skills
runs:writestore.ScopeRunsWritePOST/PUT/DELETE mutations; also satisfies runs:read
adminstore.ScopeAdminSuperscope; satisfies any scope check

Tenant isolation: API keys carry a tenant_id. Run-creation and run-lookup routes enforce that the authenticated tenant matches the resource tenant. Cross-tenant access returns HTTP 404 (not 403) to prevent resource-existence disclosure. The empty tenant "" is normalized to "default".


Health

MethodPathAuthResponse
GET/healthzNone{"status":"ok"}

Runs and Conversations

Runs are the core unit of execution in harnessd. A run accepts a prompt, invokes an LLM agent with a set of tools, streams back events, and reaches a terminal state.

Two execution models

harnessd has two distinct ways to run an agent. Choose based on whether you need streaming or a blocking call:

  • POST /v1/runs — asynchronous streaming model. Returns HTTP 202 immediately with a run_id. The caller streams real-time events from GET /v1/runs/{id}/events. This is the primary execution path used by the CLI and TUI.
  • POST /v1/agents — synchronous single-shot model. Blocks until the agent finishes (default timeout 120s, max 600s) and returns {"output":"…","summary":"…","duration_ms":N} in one response. Simpler to integrate when you do not need event streaming.

Run routes

MethodPathScopeNotes
POST/v1/runsruns:writeStart a run. Body: RunRequest. Returns HTTP 202 {"run_id":"…","status":"…"}.
GET/v1/runsruns:readList runs. Query: conversation_id, status, tenant_id. Requires persistent store (501 without it).
GET/v1/runs/{id}runs:readGet run by ID. Checks in-memory first, then store fallback. Returns a Run object.
GET/v1/runs/{id}/eventsruns:readSSE stream. Supports Last-Event-ID reconnection. Terminal events close the stream.
GET/v1/runs/{id}/summaryruns:readPost-run telemetry: steps, tokens, cost, tool calls, cache hit rate.
GET/v1/runs/{id}/contextruns:readContext window status for an active run.
GET/v1/runs/{id}/inputruns:readGet a pending ask_user_question request.
POST/v1/runs/{id}/inputruns:writeSubmit answers. Body: {"answers": {"q_id": "answer"}}. Returns HTTP 202.
GET/v1/runs/{id}/todosruns:readGet the todo list for the run.
PUT/v1/runs/{id}/todosruns:writeReplace the todo list. Body: {"todos": [...]}.
POST/v1/runs/{id}/continueruns:writeStart a new run in the same conversation. Body: {"prompt":"…","allowed_tools":[],"permissions":{}}. Returns HTTP 202.
POST/v1/runs/{id}/steerruns:writeInject a steering message into an active run. Body: {"prompt":"…"}. Returns HTTP 202 {"status":"accepted"}.
POST/v1/runs/{id}/compactruns:writeTrigger in-memory context compaction. Body: {"mode":…,"keep_last":N}. Returns {"ok":true,"messages_removed":N}.
POST/v1/runs/{id}/cancelruns:writeRequest cooperative cancellation. Returns {"status":"cancelling"}.
POST/v1/runs/{id}/approveruns:writeApprove a pending tool call (requires ApprovalBroker). Returns {"status":"approved"}.
POST/v1/runs/{id}/denyruns:writeDeny a pending tool call. Returns {"status":"denied"}.
POST/v1/runs/replayruns:writeReplay a recorded rollout. Body fields: rollout_path (required), mode ("simulate" or "fork", required), fork_step (required for fork mode), detect_drift (bool, simulate only).

Conversation routes

MethodPathScopeNotes
GET/v1/conversations/runs:readList conversations. Query: workspace, tenant_id, limit (default 50), offset. Delegates to search when q= is present.
GET/v1/conversations/searchruns:readFull-text search. Required: q=. Optional: limit (default 20), tenant_id.
GET/v1/conversations/{id}/messagesruns:readIn-memory messages for the conversation.
GET/v1/conversations/{id}/runsruns:readAll runs for a conversation.
GET/v1/conversations/{id}/exportruns:readJSONL (ndjson) export of all messages.
POST/v1/conversations/{id}/compactruns:writeReplace early messages with a summary. Body: {"keep_from_step":N,"summary":"…","role":"system"}. Auto-generates summary via LLM when summary is omitted.
POST/v1/conversations/{id}/forkruns:writeDuplicate the conversation — full message history included — under a server-minted ID. No body. Returns {"conversation_id":"…","forked_from":"…","message_count":N}. The fork inherits the source's workspace and tenant (cross-tenant requests are rejected with 404); pinned flag and token/cost counters start at zero. Works for persisted conversations and ones held only in server memory (mid-run), capturing the latest in-memory view. 404 unknown source; 405 for non-POST; 501 when conversation persistence is not configured. Afterwards the two conversations diverge independently.
POST/v1/conversations/cleanupruns:writeBulk-delete old conversations. Body: {"max_age_days":30}. Returns {"deleted":N}.
DELETE/v1/conversations/{id}runs:writeDelete a conversation.

SSE event stream

GET /v1/runs/{id}/events returns Content-Type: text/event-stream; charset=utf-8. Each frame:

id: <run_id>:<seq>
retry: 3000
event: <event_type>
data: {"id":"…","run_id":"…","type":"…","timestamp":"…","payload":{…}}

Reconnection: include Last-Event-ID: <run_id>:<seq> to replay events the client missed.

Keepalive: the server sends : ping comment lines every HARNESS_SSE_KEEPALIVE_SECONDS seconds (default 15) to keep the connection alive through proxies.

Terminal events that close the stream: run.completed, run.failed, run.cancelled.


Catalog, Models, Providers, Summarize

MethodPathScopeNotes
GET/v1/modelsruns:readReturns {"models":[{id,provider,aliases,input_cost_per_mtok,output_cost_per_mtok}]}.
GET/v1/providersruns:readReturns {"providers":[{name,configured,api_key_env,base_url,model_count}]}.
PUT/v1/providers/{name}/keyadminSet a provider API key at runtime. Body: {"key":"…"}. Returns HTTP 204.
POST/v1/summarizeruns:writeLLM-generated summary of a message list. Body: {"messages":[…],"system":"…"}. Returns {"summary":"…"}.

Skills, Agents, Subagents, Profiles

Skills

GET /v1/skills and related routes return 501 when the skills system is not configured.

MethodPathScopeNotes
GET/v1/skillsruns:readList all skills. Returns {"skills":[…]}.
GET/v1/skills/{name}runs:readGet skill by name.
POST/v1/skills/{name}/verifyruns:writeMark skill as verified. Body: {"verified_by":"api"}.

Agents (synchronous)

MethodPathScopeNotes
POST/v1/agentsruns:writeSynchronous single-shot execution. Body: {"prompt":"…"} or {"skill":"…","skill_args":"…"} plus optional allowed_tools, timeout_seconds (default 120, max 600). Blocks until done; returns {"output":"…","summary":"…","duration_ms":N}.

Subagents

All subagent routes return 501 when ServerOptions.SubagentManager is nil.

MethodPathScopeNotes
GET/v1/subagentsruns:readList subagents. Returns {"subagents":[…]}.
POST/v1/subagentsruns:writeCreate a subagent. Body: subagents.Request. Returns HTTP 201 with the subagent object.
GET/v1/subagents/{id}runs:readGet subagent by ID.
DELETE/v1/subagents/{id}runs:writeDelete subagent. Returns HTTP 409 Conflict if the subagent is still active.
POST/v1/subagents/{id}/waitruns:readLong-poll until the subagent reaches a terminal state (polls at 200ms).
POST/v1/subagents/{id}/cancelruns:writeCancel subagent. Returns {"id":"…","status":"cancelling"}.

Profiles

MethodPathScopeNotes
GET/v1/profilesruns:readList all profiles across project/user/built-in tiers. Returns {"profiles":[…],"count":N}.
GET/v1/profiles/{name}runs:readGet profile by name.
POST/v1/profiles/{name}runs:writeCreate a user profile. Returns HTTP 201. Returns 409 Conflict for built-in names. Requires ProfilesDir configured (501 otherwise).
PUT/v1/profiles/{name}runs:writeUpdate a user-tier profile. Returns 403 for built-in names.
DELETE/v1/profiles/{name}runs:writeDelete a user-tier profile. Returns 403 for built-ins.

Cron, Checkpoints, Recipes, MCP, Networks, Script Workflows, Relay

Cron jobs

harnessd embeds a cron scheduler by default (backed by SQLite at <workspace>/.harness/cron.db, concurrency cap 5). Set HARNESS_CRON_URL to delegate to a remote cronsd instance instead.

MethodPathScopeNotes
GET/v1/cron/jobsruns:readList cron jobs (tenant-filtered).
POST/v1/cron/jobsruns:writeCreate job. Body: {"name":"…","schedule":"*/5 * * * *","execution_type":"shell","execution_config":"{\"command\":\"…\"}","timeout_seconds":30}.
GET/v1/cron/jobs/{id}runs:readGet job by ID or name.
PATCH/v1/cron/jobs/{id}runs:writeUpdate job. Body: any subset of create fields; status accepts only "active" or "paused".
DELETE/v1/cron/jobs/{id}runs:writeSoft-delete job. Returns HTTP 204.
POST/v1/cron/jobs/{id}/pauseruns:writePause job.
POST/v1/cron/jobs/{id}/resumeruns:writeResume paused job.

Checkpoints

Checkpoints allow a paused run to be resumed with additional context — for example, after a human review step.

MethodPathScopeNotes
GET/v1/checkpoints/{id}runs:readGet checkpoint record.
POST/v1/checkpoints/{id}/resumeruns:writeResume a paused checkpoint. Body: {"payload":{"key":"value"}}. Returns {"status":"resumed"}.

Recipes

Recipes are reusable run definitions loaded from HARNESS_RECIPES_DIR.

MethodPathScopeNotes
GET/v1/recipesruns:readList recipe definitions.
GET/v1/recipes/{name}runs:readGet a specific recipe.
GET/v1/recipes/{name}/schemaruns:readReturns {"parameters":{…}} — the recipe's parameter schema.

MCP servers

harnessd can manage connections to external MCP servers at runtime. The MCP HTTP endpoint (/mcp) is a separate JSON-RPC surface on the same port — see the MCP documentation for details.

MethodPathScopeNotes
GET/v1/mcp/serversruns:readList connected MCP servers with their tool lists.
POST/v1/mcp/serversadminConnect a new MCP server at runtime. Body: {"url":"…","name":"…"}.

Networks

Networks are agent-graph definitions loaded from HARNESS_NETWORKS_DIR. The /v1/networks routes are always registered but return 501 when no networks engine is configured. Set HARNESS_NETWORKS_DIR (which wires ServerOptions.Networks) to activate them. Detailed network documentation is not yet published; this entry covers the route group's existence and the env var that activates it.

MethodPathScopeNotes
GET/v1/networksruns:readList network definitions. Returns 501 when networks engine is not configured.
GET/v1/networks/{name}runs:readGet a specific network definition.
POST/v1/networks/{name}/runsruns:writeStart a network run. Body: {"input":{…}}. Returns HTTP 202 {"run_id":"…","status":"…"}.

Script workflows

Script workflows are Go func(ctx *Context) (any, error) functions registered with the workflow engine and exposed over HTTP with SSE event streaming.

All script-workflow routes return 501 when ServerOptions.ScriptWorkflows is nil.

MethodPathScopeNotes
GET/v1/script-workflowsruns:readList registered script workflows.
GET/v1/script-workflows/{name}runs:readGet workflow metadata by name.
POST/v1/script-workflows/{name}/runsruns:writeStart a workflow run. Body: {"args":{…}}. Returns HTTP 202 {"run_id":"wf_…","status":"running","workflow_name":"…"}.
GET/v1/script-workflow-runs/{id}runs:readGet run status and result. Returns {"id":"wf_…","workflow_name":"…","status":"…","result_json":"…","error":"…"}.
GET/v1/script-workflow-runs/{id}/eventsruns:readSSE stream of workflow events. Historical events are replayed before live ones. Closes on workflow.completed or workflow.failed.
POST/v1/script-workflow-runs/{id}/resumeruns:writeResume a failed run. Body: {"args":{…}}. Returns HTTP 202 {"run_id":"wf_…","status":"running"}.

Relay workers

Go Relay is the multi-location control plane that routes work across registered execution environments. The worker CRUD and heartbeat routes are the HTTP surface for worker management.

All relay routes return 501 when HARNESS_RELAY_DB is not set.

MethodPathScopeNotes
GET/v1/relay/workersruns:readList registered workers. Query: status, location_type, trust_tier, tenant_id.
POST/v1/relay/workersruns:writeRegister a new worker. Setting trust_tier: "privileged" requires admin scope.
GET/v1/relay/workers/{id}runs:readGet worker by ID.
PUT/v1/relay/workers/{id}runs:writeUpdate worker fields.
DELETE/v1/relay/workers/{id}runs:writeDeregister worker.
POST/v1/relay/workers/{id}/heartbeatruns:writeSubmit heartbeat. Body: {"load":N,"status":"online"}. status must be "online" or "draining". Workers not heartbeating within 30 seconds transition to "stale".

Webhooks

Webhook routes bypass Bearer auth entirely — they authenticate via HMAC signature headers. Enable each webhook by setting the corresponding secret env var.

MethodPathAuthNotes
POST/v1/external/triggerHMAC (X-Trigger-Signature)Source-agnostic trigger. Requires ValidatorRegistry. Actions: "start", "steer", "continue".
POST/v1/webhooks/githubHMAC-SHA256 (X-Hub-Signature-256)Requires GITHUB_WEBHOOK_SECRET.
POST/v1/webhooks/slackHMAC-SHA256 (X-Slack-Signature)Requires SLACK_SIGNING_SECRET.
POST/v1/webhooks/linearHMAC-SHA256 (X-Linear-Signature)Requires LINEAR_WEBHOOK_SECRET.
MethodPathScopeNotes
POST/v1/search/coderuns:writeSourcegraph code search proxy. Requires HARNESS_SOURCEGRAPH_ENDPOINT and HARNESS_SOURCEGRAPH_TOKEN.

Schemas

RunRequestPOST /v1/runs body

Source: internal/harness/types.go.

{
"prompt": "write a hello world in Go",
"model": "gpt-4o",
"provider_name": "openai",
"workspace_type": "",
"allow_fallback": false,
"fallback_providers": [],
"system_prompt": "",
"tenant_id": "",
"conversation_id": "",
"agent_id": "",
"agent_intent": "",
"task_context": "",
"prompt_profile": "",
"prompt_extensions": {
"behaviors": [],
"talents": [],
"skills": [],
"custom": ""
},
"max_steps": 0,
"max_turns": 0,
"max_cost_usd": 0.0,
"reasoning_effort": "",
"allowed_tools": [],
"mcp_servers": [
{"name": "sqlite", "command": "uvx", "args": ["mcp-server-sqlite", "--db-path", "/tmp/my.db"]}
],
"dynamic_rules": [],
"profile": "",
"parent_context_handoff": null,
"permissions": {
"sandbox": "unrestricted",
"approval": "none"
},
"role_models": {
"primary": "",
"summarizer": ""
}
}

Selected field notes:

  • prompt is required for a direct run. Omit when using profile + skill via POST /v1/agents.
  • workspace_type accepts: "" (server default), "local", "worktree", "container", "vm".
  • max_steps and max_turns: 0 means runner default/unlimited; negative values are rejected.
  • max_cost_usd: 0 means unlimited; the run emits run.cost_limit_reached on breach (run still completes normally).
  • permissions.sandbox: "unrestricted" (default), "local", or "workspace".
  • permissions.approval: "none" (default), "destructive", or "all".
  • initiator_api_key_prefix is server-populated from the auth context — it is never accepted from the request body.

RunGET /v1/runs/{id} response

Source: internal/harness/types.go.

{
"id": "run_abc123",
"prompt": "write a hello world in Go",
"model": "gpt-4o",
"provider_name": "openai",
"status": "completed",
"output": "package main\n\nimport \"fmt\"\n\nfunc main() { fmt.Println(\"Hello, World!\") }",
"error": "",
"usage_totals": {},
"cost_totals": {},
"tenant_id": "default",
"conversation_id": "conv_xyz",
"agent_id": "",
"created_at": "2026-06-28T10:00:00Z",
"updated_at": "2026-06-28T10:00:05Z"
}

status values: running, completed, failed, cancelled, queued, waiting_for_user, waiting_for_approval.

RunSummaryGET /v1/runs/{id}/summary response

Source: internal/harness/types.go.

{
"run_id": "run_abc123",
"status": "completed",
"steps_taken": 3,
"total_prompt_tokens": 1200,
"total_completion_tokens": 400,
"total_cost_usd": 0.0018,
"cost_status": "available",
"tool_calls": [
{"tool_name": "write", "step": 2}
],
"cache_hit_rate": 0.0,
"error": ""
}

Error envelope

All non-streaming errors follow a consistent shape:

{"error": {"code": "not_found", "message": "run not found"}}

Scope errors use a slightly different shape:

{"error": "insufficient_scope", "required": "runs:write"}

Request limits and timeouts

SettingDefaultNotes
Max request body1 MiBApplies to all non-replay endpoints
Max body for replay4 MiBPOST /v1/runs/replay only
Handler timeout30sNon-streaming endpoints only
SSE keepalive15sControlled by HARNESS_SSE_KEEPALIVE_SECONDS

Streaming paths (/events, /stream, /wait suffix) bypass the 30-second handler timeout.


Key environment variables

Env varDefaultEffect
HARNESS_ADDR:8080HTTP listen address
HARNESS_AUTH_DISABLED"" (false)Set "true" to bypass all Bearer auth
HARNESS_RUN_DB""SQLite path; enables GET /v1/runs and auth
HARNESS_RELAY_DB""SQLite path; enables /v1/relay/workers routes
HARNESS_RECIPES_DIR""Enables /v1/recipes routes
HARNESS_NETWORKS_DIR""Enables /v1/networks routes
HARNESS_SSE_KEEPALIVE_SECONDS15SSE ping interval
HARNESS_CRON_URL""Set to delegate cron to external cronsd; empty uses embedded scheduler
GITHUB_WEBHOOK_SECRET""Enables /v1/webhooks/github
SLACK_SIGNING_SECRET""Enables /v1/webhooks/slack
LINEAR_WEBHOOK_SECRET""Enables /v1/webhooks/linear

Quick start (key-free smoke)

The simplest way to exercise the API without a real LLM key. The fake provider requires a turns file (HARNESS_FAKE_TURNS) — starting without one is a fatal error.

# Write a fake turns file (required by HARNESS_PROVIDER=fake)
cat > /tmp/turns.json <<'EOF'
[{"content":"smoke ok","usage":{"prompt":100,"completion":50},"cost_usd":0.001,"cost_status":"available"}]
EOF

# Start harnessd with the fake provider
HARNESS_ADDR=":8080" \
HARNESS_AUTH_DISABLED=true \
HARNESS_PROVIDER=fake \
HARNESS_FAKE_TURNS=/tmp/turns.json \
go run ./cmd/harnessd &

# Start a run
curl -s -X POST http://localhost:8080/v1/runs \
-H "Content-Type: application/json" \
-d '{"prompt":"say hello"}' | jq .

# Stream its events (replace run_abc123 with the run_id from above)
curl -s -N http://localhost:8080/v1/runs/run_abc123/events

# Check health
curl -s http://localhost:8080/healthz

Alternatively, run bash scripts/run-bench-smoke.sh which handles the turns file, build, and health-check automatically.


Next steps: see the Events reference for the full SSE event catalog, or the Script Workflows guide to learn how to register and run Go-authored pipelines.