Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

HTTP API

The runtime serves one HTTP API on --addr (default 127.0.0.1:7233). It contains the administrative endpoints, used by the orbit CLI and your applications to register workflows and manage executions, and the HTTP/JSON binding of the worker protocol, used by the SDKs. This page lists every route with its authentication requirement, request and response bodies, status codes and error codes. The examples are real responses from orbit server with examples/order_processing.orbit deployed; long values are shortened with ....

Conventions

  • Request and response bodies are JSON (content-type: application/json). Requests with a body must send that content type.
  • Timestamps are milliseconds since the Unix epoch (*_ms fields).
  • Every response that passes authentication carries the header x-orbit-api-version: orbit-api/1.
  • The API is versioned by path (/v1). Fields may be added to responses within orbit-api/1; clients should ignore fields they do not know.
  • Outside /v1/, the same address serves the web UI at /ui/ (no credential needed for its static files). GET / redirects to /ui/ (302) when the Accept header contains text/html, and otherwise answers 404 with error code NOT_FOUND. With --no-ui neither route exists.

Authentication

When the server runs without --token and without --workers-file, every endpoint is open. This is only allowed on a loopback address unless you pass --insecure.

When a credential is configured, every endpoint except GET /v1/health and GET /v1/ready requires one of:

CallerHow it authenticatesCan call
AdminAuthorization: Bearer <--token>Every endpoint.
Shared workerAuthorization: Bearer <--worker-token>/v1/workers/* and /v1/tasks/* only. No identity restrictions.
Per-worker identityAuthorization: Bearer <token> whose SHA-256 is in a [[worker]] entry of the workers file, or a client certificate (mutual TLS) matching its client_cert/v1/workers/* and /v1/tasks/* only, restricted to its own worker id, task patterns, sessions and task tokens.
Client identityAuthorization: Bearer <token> whose SHA-256 is in a [[client]] entry of the workers file, or a client certificate matching its client_certThe execution API (/v1/executions..., and GET /v1/workflows...), for the workflows and actions the entry grants.

GET /v1/workers (the worker list) is an administrative endpoint: the /v1/workers/* prefix covers only the paths below it. A request with a valid client certificate that matches no worker or client in the workers file is refused with 403 on every path, including /v1/health. See Security for the full model.

A client identity is checked per route:

RouteNeeds the action
POST /v1/executionsstart for the workflow in the body
POST /v1/executions/{id}/signals/{name}signal
GET /v1/executions/{id}/queries/{name}query
GET /v1/executions/{id}, /history, /wait, GET /v1/workflows/{name}...read
POST /v1/executions/{id}/cancelcancel
GET /v1/executions, GET /v1/workflowsnone: the list contains only workflows the client may read
POST /v1/workflows, GET /v1/workers, /v1/workers/*, /v1/tasks/*, GET /v1/metricsrefused

For routes with an execution id, the workflow is the execution’s workflow; an id that does not exist is 404 NOT_FOUND. See Security: client identities.

$ curl -s https://orbit.internal:7233/v1/executions
{"error":{"code":"UNAUTHENTICATED","message":"missing or invalid credentials"}}
$ curl -s -H "authorization: Bearer $ORBIT_WORKER_TOKEN" https://orbit.internal:7233/v1/executions
{"error":{"code":"FORBIDDEN","message":"worker credentials may only call /v1/workers/* and /v1/tasks/*"}}
$ curl -s -H "authorization: Bearer $WAREHOUSE_TOKEN" https://orbit.internal:7233/v1/metrics
{"error":{"code":"FORBIDDEN","message":"client `warehouse-app` may only use the execution API (start, signal, query, read, cancel) and schedules"}}
$ curl -s -X POST -H "authorization: Bearer $WAREHOUSE_TOKEN" https://orbit.internal:7233/v1/executions/labels-1/cancel
{"error":{"code":"FORBIDDEN","message":"client `warehouse-app` may not cancel executions of `PrintLabels`"}}

Errors

Errors produced by ORBIT have this body:

{ "error": { "code": "WORKFLOW_NOT_FOUND", "message": "workflow Nope is not registered" } }

code is stable and meant for programs; message is for people and may change. The HTTP status and the code always agree as listed below.

CodeStatusReturned byMeaning
INVALID_REQUEST400worker register, task complete/failA required field is missing or empty: worker_id is required, a worker must announce at least one task contract, token is required, error_code is required.
INVALID_IR400POST /v1/workflowsThe body is not valid IR in a supported version (orbit-ir/1 or orbit-ir/2): an unsupported ir_version, a missing field, a failed validation, a missing definition_hash, or a definition_hash that does not match the content.
CHILD_NOT_REGISTERED400POST /v1/workflowsThe definition starts a child workflow whose exact definition is not registered. Register the child first; orbit deploy does this.
INVALID_INPUT400POST /v1/executions, POST/PATCH /v1/schedulesThe workflow input (an execution’s, or a schedule’s) does not match the workflow’s input type. The message names the path, for example $.order_id.
INVALID_SCHEDULE400POST/PATCH /v1/schedulesThe schedule is not valid: an invalid id, neither or both of cron and interval, an invalid cron expression, interval, duration or time, an unknown time zone, timezone with interval, end_at not after start_at, or an unknown field. The message says which.
INVALID_EXECUTION_ID400POST /v1/executionsexecution_id is empty, longer than 200 characters, or contains characters other than letters, digits, -, _, . and :.
RESERVED_TASK400POST /v1/workers/registerA worker announced a built-in task (orbit.*) or a child-workflow contract (workflow:*).
UNAUTHENTICATED401any authenticated endpointNo credential, or a credential that matches nothing.
FORBIDDEN403any authenticated endpointThe credential is valid but not allowed: a worker credential on an administrative endpoint, a per-worker identity acting for another worker, announcing a task outside its patterns or the wrong runtime, using another worker’s session or token, a client identity outside the execution API or its granted workflows and actions, or an unmatched client certificate.
NOT_FOUND404execution endpointsThe execution does not exist.
RUN_NOT_FOUND404GET /v1/executions/{id}/history?run=NThe execution has no run N.
SCHEDULE_NOT_FOUND404/v1/schedules/{id}The schedule does not exist.
UNKNOWN_SIGNAL404POST /v1/executions/{id}/signals/{name}The execution’s workflow does not declare this signal. The message lists the declared signals.
UNKNOWN_QUERY404GET /v1/executions/{id}/queries/{name}The execution’s workflow does not declare this query. The message lists the declared queries.
WORKFLOW_NOT_FOUND404workflow endpoints, POST /v1/executionsNo such workflow, or no such version.
UNKNOWN_SESSION404heartbeat, poll, unregisterThe worker session does not exist or has expired. Register again.
DEFINITION_CONFLICT409POST /v1/workflowsA different definition is already registered under this name and version. Definitions are immutable; bump the version.
EXECUTION_EXISTS409POST /v1/executionsAn execution with this execution_id exists for a different workflow definition.
SCHEDULE_EXISTS409POST /v1/schedulesA schedule with this schedule_id exists.
EXECUTION_FINISHED409POST /v1/executions/{id}/cancel, POST /v1/executions/{id}/signals/{name}The execution is already terminal.
PAYLOAD_TOO_LARGE413POST /v1/executions, POST /v1/executions/{id}/signals/{name}, POST /v1/tasks/completeThe execution input, signal payload or task result is larger than --max-payload-bytes, as compact JSON. Nothing is recorded. See Resource limits.
SIGNAL_PAYLOAD_INVALID422POST /v1/executions/{id}/signals/{name}The payload does not match the signal’s declaration. The message names the path, for example $.approved. Nothing is recorded.
QUERY_FAILED422GET /v1/executions/{id}/queries/{name}Evaluating the query failed at runtime, for example with a division by zero.
RESOURCE_EXHAUSTED429POST /v1/executions--max-active-executions executions are already running. Nothing is started; retry later with the same execution_id.
STORAGE_ERROR500any endpoint that reads or writes the databaseThe database operation failed. The message contains the backend error. Nothing was changed. On a cluster leader that was just deposed, or in shared mode on a server that lost an execution to another server, the message starts with fenced:.
NOT_LEADER503every route except GET routes, on a cluster standbyThis server is a standby (--cluster). The body’s error object adds leader_node and leader_addr (either may be null), the x-orbit-leader header carries the address, and retry-after: 1 is set. Send the request to the leader, or retry. See Running several servers.

Some errors are produced by the HTTP layer before a request reaches ORBIT. They have a plain-text body (content-type: text/plain), not the JSON envelope:

StatusWhenExample body
400The body is not valid JSON, or a query parameter has the wrong type.Failed to parse the request body as JSON: expected ident at line 1 column 2
404Unknown path. The body is empty.
405Known path, wrong method. The allow header lists the method.
413The body is larger than --max-body. (A value larger than --max-payload-bytes gets the JSON PAYLOAD_TOO_LARGE instead.)Failed to buffer the request body: length limit exceeded
415A body was sent without content-type: application/json.
422Valid JSON with a missing or mistyped field.Failed to deserialize the JSON body into the target type: missing field `workflow` at line 1 column 12

A client should check the status first, and parse the JSON error envelope only when the body is JSON.

Routes

MethodPathAuthPurpose
GET/v1/healthnoneLiveness, versions and cluster role.
GET/v1/readynoneReadiness: 200 only where writes are served.
GET/v1/metricsadminPrometheus metrics.
POST/v1/workflowsadminRegister a compiled definition.
GET/v1/workflowsadminList definitions.
GET/v1/workflows/{name}adminLatest version of a definition.
GET/v1/workflows/{name}/{version}adminOne version of a definition.
POST/v1/executionsadminStart an execution.
GET/v1/executionsadminList executions.
GET/v1/executions/{id}adminCurrent state of an execution.
GET/v1/executions/{id}/historyadminEvent history.
GET/v1/executions/{id}/waitadminLong-poll until terminal.
POST/v1/executions/{id}/canceladminRequest cancellation.
POST/v1/executions/{id}/signals/{name}adminSend a typed signal.
GET/v1/executions/{id}/queries/{name}adminEvaluate a declared query.
POST/v1/schedulesadminCreate a schedule.
GET/v1/schedulesadminList schedules.
GET/v1/schedules/{id}adminOne schedule, with its next fire times.
PATCH/v1/schedules/{id}adminChange a schedule.
DELETE/v1/schedules/{id}adminDelete a schedule.
POST/v1/schedules/{id}/pause, /resumeadminPause or resume a schedule.
POST/v1/schedules/{id}/triggeradminStart an execution of a schedule now.
GET/v1/workersadminRegistered workers.
POST/v1/workers/registerworkerOpen a worker session.
POST/v1/workers/heartbeatworkerKeep a session alive; receive cancellations.
POST/v1/workers/pollworkerLong-poll for a task.
POST/v1/workers/unregisterworkerClose a session.
POST/v1/tasks/completeworkerReport a successful result.
POST/v1/tasks/failworkerReport a failed attempt.

“admin” means the admin token when authentication is enabled; routes under /v1/executions and GET /v1/workflows... also accept a client identity with the matching action, and routes under /v1/schedules a client identity with the schedule action for the schedule’s workflow (see Authentication). “worker” means any credential: admin, shared worker token or a per-worker identity.

Health and metrics

GET /v1/health

Liveness and version information. It never requires authentication. A single server binds its listener only after it has opened the database and recovered active executions, so a 200 also means the runtime is ready to serve. In cluster mode every server, leader or standby, answers 200; use /v1/ready to find the leader, or in shared mode the servers that are live.

Response 200:

{
  "status": "ok",
  "api_version": "orbit-api/1",
  "runtime_version": "0.2.0",
  "ir_versions": ["orbit-ir/1", "orbit-ir/2"],
  "active_executions": 0,
  "worker_sessions": 0,
  "role": "single",
  "node_id": null,
  "term": null,
  "leader": null
}

A cluster standby:

{"status":"ok","api_version":"orbit-api/1","runtime_version":"0.2.0","ir_versions":["orbit-ir/1","orbit-ir/2"],"active_executions":0,"worker_sessions":0,"role":"standby","node_id":"b","term":1,"leader":{"node_id":"a","addr":"http://127.0.0.1:7301"}}
FieldTypeMeaning
statusstringAlways ok.
api_versionstringorbit-api/1.
runtime_versionstringVersion of the orbit binary.
ir_versionsarray of stringsIR versions this runtime accepts. ORBIT 0.1 lists only orbit-ir/1; workflows that use signals, queries, await or compute steps, lambdas, on handlers or a limits block need a runtime that lists orbit-ir/2.
active_executionsintegerExecutions not yet terminal, held in memory.
worker_sessionsintegerWorker sessions the runtime remembers, including ones that recently went silent.
rolestringsingle without --cluster; leader or standby in cluster mode.
node_idstring or nullThe server’s --node-id; null without --cluster.
terminteger or nullThe leadership term this server last saw; null without --cluster.
leaderobject or nullThe current leader as {"node_id": ..., "addr": ...}, or null.

A server in shared mode (--cluster --cluster-mode shared) adds the fields below; term and leader are null, and active_executions counts the executions this server owns:

{"status":"ok","api_version":"orbit-api/1","runtime_version":"0.3.0","ir_versions":["orbit-ir/1","orbit-ir/2"],"active_executions":0,"worker_sessions":2,"role":"shared","node_id":"node-1","term":null,"leader":null,"owned_executions":0,"tasks_claimed":18,"nodes":[{"node_id":"node-1","addr":"http://127.0.0.1:46825","live":true,"heartbeat_age_ms":2,"owned_executions":0},{"node_id":"node-3","addr":"http://127.0.0.1:47377","live":true,"heartbeat_age_ms":248,"owned_executions":0}]}
FieldTypeMeaning
rolestringshared.
owned_executionsintegerRunning executions this server owns: it ticks them and is the only one that appends to their history.
tasks_claimedintegerTasks this server handed to workers from the shared queue since it started.
nodesarrayEvery server that is live or still owns executions: node_id, addr (its --advertise-addr), live (heartbeat younger than --node-timeout), heartbeat_age_ms and owned_executions. Refreshed every second.

GET /v1/ready

Readiness for load balancers and orchestrators. It never requires authentication. It returns 200 on a single server and on a cluster leader, and 503 on a cluster standby, so a load balancer that checks it sends traffic only to the server that accepts writes.

Response 200 (single server):

{"role":"single","ready":true}

Response 503 (cluster standby):

{"role":"standby","node_id":"b","term":1,"leader":{"node_id":"a","addr":"http://127.0.0.1:7301"},"ready":false}

A leader that cannot confirm its leadership within its lease (3 seconds) answers 503 until it can. See Running several servers.

In shared mode every live server answers 200, with the shared-mode fields of /v1/health and "ready": true. A server answers 503 while it resumes the executions it owned before a restart, when it could not write its heartbeat within --node-timeout, and when another running server uses the same --node-id.

GET /v1/metrics

Prometheus text exposition (content-type: text/plain; version=0.0.4). Observability lists every metric.

# HELP orbit_executions_started_total Executions started
# TYPE orbit_executions_started_total counter
orbit_executions_started_total 8
...
# HELP orbit_active_executions Executions not yet finished
# TYPE orbit_active_executions gauge
orbit_active_executions 0

Schedules and continue-as-new add these counters (from a server that ran the examples of Schedules):

# HELP orbit_executions_continued_as_new_total Runs ended by continue_as_new
# TYPE orbit_executions_continued_as_new_total counter
orbit_executions_continued_as_new_total 2
# HELP orbit_schedules_fired_total Schedule occurrences that started an execution
# TYPE orbit_schedules_fired_total counter
orbit_schedules_fired_total 5
# HELP orbit_schedules_skipped_total Schedule occurrences that started nothing
# TYPE orbit_schedules_skipped_total counter
orbit_schedules_skipped_total{reason="overlap"} 0
orbit_schedules_skipped_total{reason="missed"} 0

Workflow definitions

POST /v1/workflows

Register a compiled workflow definition: the orbit-ir/1 or orbit-ir/2 JSON produced by orbit compile. Registering identical content again is a no-op.

Request body: the IR document. Abbreviated:

{
  "ir_version": "orbit-ir/1",
  "name": "OrderProcessing",
  "version": "1.0.0",
  "description": "Validate, risk-score, reserve, charge and notify for one order",
  "definition_hash": "sha256:4fdfac082bf3f2fbbbc8ac5465d37cba01dffd1b8e4ae08e25be3e0d76486c41",
  "timeout_ms": 300000,
  "input": { "kind": "record", "fields": [ ... ] },
  "output": { ... },
  "steps": [ ... ],
  "result": { ... },
  "contracts": { ... },
  "source_map": { ... }
}

Responses:

StatusBodyWhen
201{"name", "version", "definition_hash", "created": true}The definition is new.
200the same, with "created": falseAn identical definition is already registered.
400INVALID_IR, CHILD_NOT_REGISTERED
409DEFINITION_CONFLICT
{"name":"OrderProcessing","version":"1.0.0","definition_hash":"sha256:4fdfac082bf3f2fbbbc8ac5465d37cba01dffd1b8e4ae08e25be3e0d76486c41","created":false}
{"error":{"code":"INVALID_IR","message":"definition_hash does not match the IR content (was the file modified?)"}}

GET /v1/workflows

List every registered definition (all versions), sorted by name, then by version string.

Response 200:

{
  "workflows": [
    {
      "name": "OrderProcessing",
      "version": "1.0.0",
      "description": "Validate, risk-score, reserve, charge and notify for one order",
      "definition_hash": "sha256:4fdfac082bf3f2fbbbc8ac5465d37cba01dffd1b8e4ae08e25be3e0d76486c41",
      "steps": ["validate_order", "analyze_risk", "reserve_inventory", "process_payment", "send_notification"],
      "tasks": ["inventory.release", "inventory.reserve", "notifications.send", "orders.validate",
                "payments.charge", "payments.refund", "risk.analyze"]
    }
  ]
}

description is null when the workflow has none. tasks lists the contract keys the definition uses, including built-in tasks and child workflows (workflow:<Name>).

GET /v1/workflows/{name}

The full IR of the highest registered version of name, ordered by semantic version. Response 200: the IR document. Error 404 WORKFLOW_NOT_FOUND (workflow not found).

GET /v1/workflows/{name}/{version}

The full IR of one version. Response 200: the IR document. Error 404 WORKFLOW_NOT_FOUND.

Executions

POST /v1/executions

Start an execution.

FieldTypeRequiredMeaning
workflowstringyesWorkflow name.
versionstringnoVersion to start. Default: the highest registered version.
inputanynoWorkflow input. null or absent means {}. Defaults declared in the workflow’s input are applied, then the input is validated strictly.
execution_idstringnoClient-chosen id. Default: a random UUID. Starting an id that already exists for the same definition returns the existing execution.
{
  "workflow": "OrderProcessing",
  "version": "1.0.0",
  "execution_id": "order-1001",
  "input": {"order_id": "1001", "customer_id": "c-42", "amount": 99.5, "items": [{"sku": "BOOK-1", "quantity": 1}]}
}

Responses:

StatusBodyWhen
201{"execution_id": "order-1001", "created": true}A new execution was started.
200{"execution_id": "order-1001", "created": false}The id exists for the same definition. Nothing was started.
400INVALID_INPUT, INVALID_EXECUTION_ID
404WORKFLOW_NOT_FOUND
409EXECUTION_EXISTSThe id exists for a different definition.
413PAYLOAD_TOO_LARGEThe input, with defaults applied, is larger than --max-payload-bytes.
429RESOURCE_EXHAUSTED--max-active-executions is reached. An id that already exists still gets 200.
{"error":{"code":"INVALID_INPUT","message":"workflow input is invalid at $.order_id: expected String, found an integer"}}
{"error":{"code":"WORKFLOW_NOT_FOUND","message":"workflow OrderProcessing@9.9.9 is not registered"}}
{"error":{"code":"EXECUTION_EXISTS","message":"execution `order-1001` already exists for a different workflow"}}
{"error":{"code":"PAYLOAD_TOO_LARGE","message":"execution input is 414 bytes; the limit is 200 (--max-payload-bytes or the workflow's `limits`)"}}
{"error":{"code":"RESOURCE_EXHAUSTED","message":"2 executions are already running (--max-active-executions)"}}

GET /v1/executions

List executions, newest first.

Query parameterTypeDefaultMeaning
statusstringallFilter by status, case-insensitive (failed matches FAILED).
workflowstringallFilter by workflow name.
limitinteger50Maximum rows, clamped to 1 through 1000.

For a client identity, rows of workflows it may not read are removed after the limit is applied, so it can receive fewer rows than limit; filter by workflow to get full pages.

Response 200:

{
  "executions": [
    {
      "execution_id": "order-1002",
      "workflow": "OrderProcessing",
      "version": "1.0.0",
      "status": "FAILED",
      "created_at_ms": 1794735367702,
      "updated_at_ms": 1794735369299
    }
  ]
}

GET /v1/executions/{id}

The current state of an execution, derived from its history. Error 404 NOT_FOUND.

Response 200 (execution order-1002, which failed and was compensated; some units shortened):

{
  "id": "order-1002",
  "workflow": "OrderProcessing",
  "version": "1.0.0",
  "definition_hash": "sha256:4fdfac082bf3f2fbbbc8ac5465d37cba01dffd1b8e4ae08e25be3e0d76486c41",
  "input": {"order_id": "1002", "customer_id": "c-42", "amount": 99.5,
            "items": [{"sku": "BOOK-1", "quantity": 1}], "currency": "USD"},
  "status": "FAILED",
  "created_at_ms": 1794735367702,
  "updated_at_ms": 1794735369299,
  "finished_at_ms": 1794735369299,
  "units": {
    "process_payment": {
      "status": "FAILED",
      "attempt": 1,
      "input": {"order_id": "1002", "amount": 99.5, "currency": "USD"},
      "idempotency_key": "charge-1002",
      "error": {"code": "CARD_DECLINED", "message": "card was declined", "retryable": false,
                "details": {"decline_code": "insufficient_funds"}},
      "last_error": {"code": "CARD_DECLINED", "message": "card was declined", "retryable": false,
                     "details": {"decline_code": "insufficient_funds"}},
      "redelivery": false,
      "scheduled_at_ms": 1794735368974,
      "dispatched_at_ms": 1794735369286,
      "deadline_ms": 1794735399286,
      "finished_at_ms": 1794735369290,
      "worker_id": "py-1",
      "worker_session": "30e57815-e28e-4a2b-9c35-df96774668ae",
      "tokens": [["3d932a10-600d-4989-acc7-5234a3183b06", 1]],
      "dispatch_count": 1
    },
    "reserve_inventory": {"status": "SUCCEEDED", "attempt": 1, "output": {"reservation_id": "res-77"}, "...": "..."},
    "compensate:reserve_inventory": {"status": "SUCCEEDED", "attempt": 1, "output": {"released": true}, "...": "..."},
    "send_notification": {"status": "CANCELLED", "attempt": 0, "redelivery": false,
                          "finished_at_ms": 1794735369290, "dispatch_count": 0}
  },
  "abort": {"kind": "failed", "error": {"code": "CARD_DECLINED",
            "message": "step `process_payment` failed: card was declined", "retryable": false,
            "details": {"decline_code": "insufficient_funds"}}},
  "compensation_order": ["reserve_inventory"],
  "compensation": "completed",
  "error": {"code": "CARD_DECLINED", "message": "step `process_payment` failed: card was declined",
            "retryable": false, "details": {"decline_code": "insufficient_funds"}},
  "last_seq": 27
}

Execution fields:

FieldTypeMeaning
idstringExecution id.
runinteger, optionalThe run this state describes, when it is not run 1 (continue-as-new). The state is always the latest run’s.
parentobject, optionalFor a child workflow: {"execution_id", "step"} of the parent unit that started it.
schedule_idstring, optionalThe schedule that started the execution.
workflow, version, definition_hashstringThe definition the execution is pinned to.
inputanyInput after defaults were applied.
statusstringRUNNING, COMPENSATING, COMPLETED, FAILED, CANCELLED or TIMED_OUT. (CONTINUED_AS_NEW only in the state of an archived run, rebuilt from its history.)
created_at_ms, updated_at_msintegerFirst and latest event times of the run.
finished_at_msinteger, optionalSet when terminal.
unitsobjectUnit state by unit key (step, step[i], compensate:<unit>). Only units that have been touched appear.
abortobject, optionalWhy the execution is being wound down: {"kind": "failed", "error"}, {"kind": "cancelled", "reason"} or {"kind": "timed_out"}.
compensation_orderarray, optionalUnits being compensated, in order.
signalsarray, optionalThe signal buffer: every signal received, in arrival order. Absent when none was received. See below.
compensationstring, optionalnone, completed or failed. Set when terminal after an abort.
outputany, optionalWorkflow output, when COMPLETED.
errorobject, optional{"code", "message", "retryable", "details"?}, when FAILED.
last_seqintegerSequence number of the latest event.

Unit fields:

FieldTypeMeaning
statusstringSee Unit statuses. Absent means PENDING.
attemptintegerCurrent attempt number, starting at 1. 0 for a unit that never started.
inputany, optionalEvaluated input. For a for step: the list of items.
idempotency_keystring, optionalKey sent to workers.
outputany, optionalResult of a succeeded unit. null for a skipped one.
errorobject, optionalFinal error of a failed unit.
last_errorobject, optionalError of the latest failed attempt.
redeliverybooleanThe current attempt is a redelivery of an uncertain one.
scheduled_at_ms, dispatched_at_ms, finished_at_msinteger, optionalTransition times.
deadline_msinteger, optionalWhen the in-flight attempt times out.
wake_at_msinteger, optionalWhen a sleep timer fires, a retry becomes due, or an await step times out.
worker_id, worker_sessionstring, optionalWho holds or held the latest dispatch. Child workflows show orbit-runtime and child:<child id>; built-in tasks orbit-runtime and builtin:<instance>.
tokenstring, optionalToken of the in-flight dispatch.
tokensarray, optionalEvery [token, attempt] issued for the unit.
succeeded_seqinteger, optionalSequence number of the success, used to order compensation.
dispatch_countintegerNumber of dispatches, including redeliveries.

Signal fields (signals[]):

FieldTypeMeaning
seqintegerSequence number of the SignalReceived event. Identifies the signal.
namestringSignal name.
payloadobjectValidated payload, with defaults applied.
received_at_msintegerWhen the signal was recorded.
delivered_tostring, optionalThe await unit that took the signal. Absent while it is buffered.
"signals": [
  {"seq": 5, "name": "review", "payload": {"approved": true, "reviewer": "dana", "comment": "verified by phone"},
   "received_at_ms": 1794746714669, "delivered_to": "decision"}
]

GET /v1/executions/{id}/history

The full event history of the latest run, in commit order. Error 404 NOT_FOUND. Executions and history describes every event type.

Query parameterTypeDefaultMeaning
runintegerthe latest runThe history of this run of the execution (continue-as-new). Error 404 RUN_NOT_FOUND for a run that does not exist.

The response has run when the history is not run 1’s or when run was given.

Response 200 (first and last events of order-1002):

{
  "execution_id": "order-1002",
  "events": [
    {"seq": 1, "timestamp_ms": 1794735367702, "type": "WorkflowCreated", "workflow": "OrderProcessing",
     "version": "1.0.0", "definition_hash": "sha256:4fdf...6c41",
     "input": {"order_id": "1002", "customer_id": "c-42", "amount": 99.5,
               "items": [{"sku": "BOOK-1", "quantity": 1}], "currency": "USD"}},
    {"seq": 2, "timestamp_ms": 1794735367702, "type": "WorkflowStarted"},
    {"seq": 3, "timestamp_ms": 1794735367703, "type": "StepScheduled", "step": "validate_order", "attempt": 1,
     "input": {"order_id": "1002", "amount": 99.5, "items": [{"sku": "BOOK-1", "quantity": 1}]},
     "idempotency_key": "order-1002/validate_order", "redelivery": false},
    "...",
    {"seq": 26, "timestamp_ms": 1794735369299, "type": "CompensationCompleted"},
    {"seq": 27, "timestamp_ms": 1794735369299, "type": "WorkflowFailed",
     "error": {"code": "CARD_DECLINED", "message": "step `process_payment` failed: card was declined",
               "retryable": false, "details": {"decline_code": "insufficient_funds"}},
     "compensation": "completed"}
  ]
}

The history of an earlier run ends with WorkflowContinuedAsNew. Response 200 for GET /v1/executions/count-1/history?run=1 (the Countdown workflow of Continue-as-new):

{
    "execution_id": "count-1",
    "events": [
        {
            "seq": 1,
            "timestamp_ms": 1794768604803,
            "type": "WorkflowCreated",
            "workflow": "Countdown",
            "version": "1.0.0",
            "definition_hash": "sha256:fb3ef66116e32f38464f39b5a1ba617a8a94583b9c1a9a7ce77035d07849d709",
            "input": {
                "n": 3
            }
        },
        {
            "seq": 2,
            "timestamp_ms": 1794768604803,
            "type": "WorkflowStarted"
        },
        {
            "seq": 3,
            "timestamp_ms": 1794768604804,
            "type": "StepSucceeded",
            "step": "tick",
            "attempt": 1,
            "output": {
                "left": 2
            }
        },
        {
            "seq": 4,
            "timestamp_ms": 1794768604804,
            "type": "WorkflowContinuedAsNew",
            "next_run": 2,
            "input": {
                "n": 2
            }
        }
    ],
    "run": 1
}
$ curl -s "localhost:7233/v1/executions/count-1/history?run=7"
{"error":{"code":"RUN_NOT_FOUND","message":"execution `count-1` has runs 1 to 3; there is no run 7"}}

The history endpoint returns values unredacted. orbit logs --json redacts them.

GET /v1/executions/{id}/wait

Long-poll until the execution is terminal, or until the timeout passes, then return the same body as GET /v1/executions/{id}. Check status to tell the two apart.

Query parameterTypeDefaultMeaning
timeout_msinteger30000Maximum wait, capped at 300000.

Error 404 NOT_FOUND.

POST /v1/executions/{id}/cancel

Request cancellation. The body is optional.

FieldTypeDefaultMeaning
reasonstringrequested via APIRecorded in CancelRequested.

Response 200: the execution state right after the request. Requesting cancellation of an execution that is already being aborted (by an earlier cancel, a failure or a timeout) records nothing and returns the current state.

{"id":"order-2001","workflow":"OrderProcessing","version":"1.0.0","...":"...","status":"CANCELLED","compensation":"none","...":"..."}

Errors: 404 NOT_FOUND; 409 EXECUTION_FINISHED:

{"error":{"code":"EXECUTION_FINISHED","message":"execution is already CANCELLED"}}

POST /v1/executions/{id}/signals/{name}

Send the signal name to an execution. The body is the payload, a JSON object; a request without a body sends {}. The runtime applies the declared defaults, validates the payload against the workflow’s signal declaration, records SignalReceived and buffers the signal until an await step takes it. See Signals and queries.

curl -X POST http://127.0.0.1:7233/v1/executions/order-A-100/signals/review \
  -H 'content-type: application/json' \
  -d '{"approved": true, "reviewer": "dana"}'

Response 200:

{"execution_id": "order-A-100", "signal": "review", "accepted": true, "seq": 5}
FieldTypeMeaning
execution_idstringThe execution.
signalstringThe signal name.
acceptedbooleanAlways true in a 200 response.
seqintegerSequence number of the recorded SignalReceived event. In shared mode, a signal sent to a server that does not own the execution is passed to the owner, and the response waits for it; seq is null when the owner has not recorded the signal within 10 seconds (it is still recorded, once, when the owner gets to it).

Errors: 404 NOT_FOUND; 409 EXECUTION_FINISHED (checked first); 404 UNKNOWN_SIGNAL; 413 PAYLOAD_TOO_LARGE; 422 SIGNAL_PAYLOAD_INVALID:

{"error":{"code":"UNKNOWN_SIGNAL","message":"OrderApproval@1.0.0 has no signal `reveiw`; declared: review"}}
{"error":{"code":"PAYLOAD_TOO_LARGE","message":"signal payload is 277 bytes; the limit is 200 (--max-payload-bytes or the workflow's `limits`)"}}
{"error":{"code":"SIGNAL_PAYLOAD_INVALID","message":"payload for signal `review` is invalid at $.approved: expected Bool, found a string"}}

A body sent without content-type: application/json, or a body that is not valid JSON, is rejected by the HTTP layer with a plain-text 415 or 400 (see Errors).

GET /v1/executions/{id}/queries/{name}

Evaluate the query name against the execution’s current state. Steps that have not succeeded read as null. Nothing is recorded. Works on running and terminal executions.

Response 200:

{"execution_id":"order-A-100","query":"status","value":{"needs_approval":true,"approved":true,"reviewer":"dana","charged":true}}
FieldTypeMeaning
execution_idstringThe execution.
querystringThe query name.
valueanyThe query result, of the query’s declared type.

Errors: 404 NOT_FOUND; 404 UNKNOWN_QUERY; 422 QUERY_FAILED (evaluating query `<name>`: <cause>):

{"error":{"code":"UNKNOWN_QUERY","message":"OrderApproval@1.0.0 has no query `stat`; declared: status"}}

Schedules

A schedule starts executions of a workflow on a cron expression or a fixed interval. Schedules explains timing, the overlap and catch-up policies and how several servers share the work. Schedules are durable (stored in the database) and every change is effective at once on every server. On a cluster standby, GET routes work and changes answer 503 NOT_LEADER.

POST /v1/schedules

Create a schedule. Request fields:

FieldTypeRequiredMeaning
schedule_idstringyesLetters, digits, -, _ and ., at most 100 characters.
workflowstringyesThe workflow to start. It must be registered.
versionstringnoPin a version. Default: the latest registered version at each fire.
inputobjectnoExecution input (default {}), validated against the workflow’s input type when the schedule is created or changed, and again at each start.
cronstringone of cron or intervalFive fields, minute hour day-of-month month day-of-week, or @hourly, @daily (@midnight), @weekly, @monthly, @yearly (@annually). See Cron expressions.
intervalstringone of cron or intervalA duration such as 30s, 15m, 1h30m or 1d (units ms, s, m, h, d; at least 1s). Occurrences are aligned to start_at, or to the Unix epoch.
timezonestringnoIANA time zone for cron, such as Europe/Berlin. Default UTC. Not allowed with interval.
overlapstringnoskip (default), allow or cancel_previous.
catchupstringnoskip_missed (default) or run_once.
start_at, end_atstringnoRFC 3339 times bounding the occurrences.
jitterstringnoA duration up to 24h: each fire is delayed by a deterministic amount below it.
descriptionstringnoFree text.
pausedbooleannoCreate the schedule paused. Default false.

Unknown fields are rejected. Response 201 with the schedule (the same document as GET /v1/schedules/{id}):

$ curl -s -X POST localhost:7233/v1/schedules -H 'content-type: application/json' -d '{"schedule_id": "orders-every-10s", "workflow": "OrderProcessing", "interval": "10s", "input": {"order_id": "R-1", "customer_id": "cust-1", "amount": 120.0, "items": [{"sku": "BOOK-1", "quantity": 1}]}}'
{"schedule_id":"orders-every-10s","workflow":"OrderProcessing","input":{"order_id":"R-1","customer_id":"cust-1","amount":120.0,"items":[{"sku":"BOOK-1","quantity":1}]},"interval":"10s","overlap":"skip","catchup":"skip_missed","paused":false,"next_fire_at":"2026-11-15T18:51:10Z","next_fire_times":["2026-11-15T18:51:10Z","2026-11-15T18:51:20Z","2026-11-15T18:51:30Z","2026-11-15T18:51:40Z","2026-11-15T18:51:50Z"],"last_fire_at":null,"last_execution_id":null,"fired":0,"skipped":0,"last_error":null,"created_at":"2026-11-15T18:51:00.347Z","updated_at":"2026-11-15T18:51:00.347Z"}

Errors: 400 INVALID_SCHEDULE, 400 INVALID_INPUT, 404 WORKFLOW_NOT_FOUND, 409 SCHEDULE_EXISTS.

{"error":{"code":"INVALID_SCHEDULE","message":"cron `0 25 * * *`: `25` is out of range for the hour field (0-23)"}}
{"error":{"code":"INVALID_INPUT","message":"the schedule input is invalid for OrderProcessing@1.0.0 at $.order_id: expected String, found an integer"}}
{"error":{"code":"SCHEDULE_EXISTS","message":"schedule `nightly-orders` already exists"}}

GET /v1/schedules/{id}

Response 200, thirty seconds after the schedule above was created:

{
    "schedule_id": "orders-every-10s",
    "workflow": "OrderProcessing",
    "input": {
        "order_id": "R-1",
        "customer_id": "cust-1",
        "amount": 120.0,
        "items": [
            {
                "sku": "BOOK-1",
                "quantity": 1
            }
        ]
    },
    "interval": "10s",
    "overlap": "skip",
    "catchup": "skip_missed",
    "paused": false,
    "next_fire_at": "2026-11-15T18:51:40Z",
    "next_fire_times": [
        "2026-11-15T18:51:40Z",
        "2026-11-15T18:51:50Z",
        "2026-11-15T18:52:00Z",
        "2026-11-15T18:52:10Z",
        "2026-11-15T18:52:20Z"
    ],
    "last_fire_at": "2026-11-15T18:51:30Z",
    "last_execution_id": "orders-every-10s:2026-11-15T18:51:30Z",
    "fired": 3,
    "skipped": 0,
    "last_error": null,
    "created_at": "2026-11-15T18:51:00.347Z",
    "updated_at": "2026-11-15T18:51:30.409Z"
}

The specification fields are those of POST /v1/schedules (optional fields that are not set are absent). The state fields:

FieldTypeMeaning
pausedbooleanThe schedule does not fire.
next_fire_atstring or nullWhen the next occurrence fires: the occurrence plus its jitter. null while paused and after end_at.
next_fire_timesarray of stringsThe next five occurrences (without jitter). Empty while paused and after end_at.
last_fire_atstring or nullThe latest occurrence handled (started or skipped). Triggers do not change it.
last_execution_idstring or nullThe latest execution the schedule started, including triggers. The overlap policy looks at this execution.
firedintegerExecutions started, including triggers.
skippedintegerOccurrences that started nothing: skipped by the overlap policy or missed while no server could fire them.
last_errorstring or nullWhy the latest start failed (for example RESOURCE_EXHAUSTED); the occurrence is retried every 5 seconds. Cleared by the next successful fire and by every change.
created_at, updated_atstringRFC 3339, UTC.

Error 404 SCHEDULE_NOT_FOUND:

{"error":{"code":"SCHEDULE_NOT_FOUND","message":"schedule `orders-every-10s` not found"}}

GET /v1/schedules

Every schedule, by id: {"schedules": [ ... ]}, each in the form of GET /v1/schedules/{id}. A client identity sees only the schedules of workflows it may schedule.

{
    "schedules": [
        {
            "schedule_id": "nightly-orders",
            "workflow": "OrderProcessing",
            "input": {
                "order_id": "N-1",
                "customer_id": "cust-1",
                "amount": 120.0,
                "items": [
                    {
                        "sku": "BOOK-1",
                        "quantity": 1
                    }
                ]
            },
            "cron": "0 2 * * *",
            "timezone": "Europe/Berlin",
            "overlap": "skip",
            "catchup": "skip_missed",
            "description": "Nightly reconciliation order",
            "paused": false,
            "next_fire_at": "2026-11-16T01:00:00Z",
            "next_fire_times": [
                "2026-11-16T01:00:00Z",
                "2026-11-17T01:00:00Z",
                "2026-11-18T01:00:00Z",
                "2026-11-19T01:00:00Z",
                "2026-11-20T01:00:00Z"
            ],
            "last_fire_at": null,
            "last_execution_id": "nightly-orders:manual:2026-11-15T18:52:22.296Z",
            "fired": 1,
            "skipped": 0,
            "last_error": null,
            "created_at": "2026-11-15T18:50:54.213Z",
            "updated_at": "2026-11-15T18:52:22.296Z"
        }
    ]
}

PATCH /v1/schedules/{id}

Change a schedule. The body is a JSON object with the fields to change, in the form of POST /v1/schedules; null removes an optional field, and paused pauses or resumes. The result is validated like a new schedule, the next occurrence is recomputed from now, and fired, skipped, last_fire_at and last_execution_id are kept. schedule_id cannot change. To switch from an interval to cron, remove interval and set cron in one request. Response 200 with the schedule (shortened):

$ curl -s -X PATCH localhost:7233/v1/schedules/orders-every-10s -H 'content-type: application/json' -d '{"interval": null, "cron": "*/15 * * * *", "overlap": "cancel_previous"}'
{
    "schedule_id": "orders-every-10s",
    "cron": "*/15 * * * *",
    "overlap": "cancel_previous",
    "next_fire_at": "2026-11-15T19:00:00Z",
    "next_fire_times": [
        "2026-11-15T19:00:00Z",
        "2026-11-15T19:15:00Z",
        "2026-11-15T19:30:00Z",
        "2026-11-15T19:45:00Z",
        "2026-11-15T20:00:00Z"
    ],
    "fired": 5,
    "skipped": 0
}

Errors as for POST /v1/schedules, and 404 SCHEDULE_NOT_FOUND.

DELETE /v1/schedules/{id}

Delete a schedule. Executions it started are not affected. Response 200:

{"schedule_id":"orders-every-10s","deleted":true}

Error 404 SCHEDULE_NOT_FOUND.

POST /v1/schedules/{id}/pause and /resume

The same as PATCH with {"paused": true} or {"paused": false}; no body is needed. Resuming computes the next occurrence from now: occurrences that passed while the schedule was paused are not started. Response 200 with the schedule (shortened):

{
    "schedule_id": "orders-every-10s",
    "paused": true,
    "next_fire_at": null,
    "next_fire_times": []
}
{
    "schedule_id": "orders-every-10s",
    "paused": false,
    "next_fire_at": "2026-11-15T18:51:40Z",
    "fired": 4
}

POST /v1/schedules/{id}/trigger

Start an execution of the schedule now, with its workflow, version and input, whether it is paused or not. The execution id is <schedule_id>:manual:<now, RFC 3339 with milliseconds>. The overlap policy applies: with skip, nothing starts while the schedule’s previous execution runs. The schedule’s timing does not change. Response 200:

{
    "schedule_id": "orders-every-10s",
    "started": true,
    "execution_id": "orders-every-10s:manual:2026-11-15T18:51:38.514Z"
}

When the overlap policy skips it: {"schedule_id": "...", "started": false, "skipped": "overlap"}.

Workers

GET /v1/workers

Every worker id that has ever registered, with its latest registration.

Response 200:

{
  "workers": [
    {
      "worker_id": "ts-1",
      "runtime": "typescript",
      "sdk": "curl",
      "contracts": [
        {"task_name": "orders.validate", "contract_version": "1.0.0"},
        {"task_name": "inventory.reserve", "contract_version": "1.0.0"}
      ],
      "last_seen_ms": 1794735369613,
      "registered_at_ms": 1794735367705,
      "online": false,
      "sessions": 0,
      "inflight": 0
    }
  ]
}
FieldTypeMeaning
worker_id, runtime, sdkstringAs announced at registration; runtime is lowercased, sdk may be null.
contractsarrayAnnounced {task_name, contract_version} pairs.
last_seen_ms, registered_at_msintegerLatest contact and latest registration.
onlinebooleanAt least one session contacted the runtime within the session timeout.
sessionsintegerLive sessions.
inflightintegerTasks held by live sessions.

Worker protocol

These endpoints are the HTTP/JSON binding of orbit.worker.v1.WorkerService. The Worker protocol reference describes the lifecycle and the rules an implementation must follow; this section gives the exact bodies.

POST /v1/workers/register

Open a worker session.

FieldTypeRequiredMeaning
worker_idstringyesStable worker identity. Must not be empty.
runtimestringyesWorker language, matched case-insensitively against a step’s using <language>.
sdkstringnoFree-form SDK name and version.
contractsarrayyesAt least one {task_name, contract_version}.
{"worker_id": "py-1", "runtime": "python",
 "contracts": [{"task_name": "risk.analyze", "contract_version": "1.0.0"},
               {"task_name": "payments.charge", "contract_version": "1.0.0"}]}

Response 200:

{"accepted": true, "worker_session_id": "01081a88-bd5b-415f-9032-07d2bec51746",
 "heartbeat_interval_ms": 5000, "session_timeout_ms": 30000, "warnings": []}

heartbeat_interval_ms is one sixth of the session timeout, and at least 500. warnings lists contracts announced at a version that no registered workflow uses, which this worker will therefore never receive:

{"accepted": true, "worker_session_id": "04586a02-...", "heartbeat_interval_ms": 5000, "session_timeout_ms": 30000,
 "warnings": ["`risk.analyze` is announced at version 0.9.0 but registered workflows use 1.0.0; this worker will not receive those tasks"]}

Errors: 400 INVALID_REQUEST, 400 RESERVED_TASK, 403 FORBIDDEN (per-worker identities):

{"error":{"code":"FORBIDDEN","message":"this credential belongs to worker `payments-1`, not `other`"}}
{"error":{"code":"FORBIDDEN","message":"worker `payments-1` must announce runtime `python`"}}
{"error":{"code":"FORBIDDEN","message":"worker `payments-1` may not serve: inventory.reserve (allowed: payments.*, audit.log)"}}
{"error":{"code":"RESERVED_TASK","message":"`orbit.http.request` is a built-in task"}}

POST /v1/workers/heartbeat

Keep a session alive and learn which tasks to stop.

FieldTypeRequiredMeaning
worker_session_idstringyesSession from register.

Response 200:

{"accepted": true, "cancel": ["5503f137-059e-455a-92c1-e65fa3690962"]}

cancel lists tokens this session holds that no longer matter: the step was resolved by another attempt, the attempt timed out, or the workflow is aborting. Stop that work if you can; results for those tokens are ignored.

Errors: 404 UNKNOWN_SESSION; 403 FORBIDDEN when a per-worker identity uses another worker’s session.

POST /v1/workers/poll

Long-poll for a task. The runtime hands out the oldest queued task that matches one of the session’s contracts (same task name and contract version, and the same runtime when the step says using <language>).

FieldTypeRequiredMeaning
worker_session_idstringyesSession from register.
wait_msintegernoMaximum wait. Default 20000, capped at 60000.

Response 200: a task.

{
  "token": "3d932a10-600d-4989-acc7-5234a3183b06",
  "execution_id": "order-1002",
  "workflow": "OrderProcessing",
  "workflow_version": "1.0.0",
  "step_id": "process_payment",
  "task_name": "payments.charge",
  "contract_version": "1.0.0",
  "input": {"order_id": "1002", "amount": 99.5, "currency": "USD"},
  "idempotency_key": "charge-1002",
  "attempt": 1,
  "redelivery": false,
  "deadline_ms": 1794735399286,
  "timeout_ms": 30000
}
FieldTypeMeaning
tokenstringIdentifies this dispatch. Send it with the result.
execution_id, workflow, workflow_versionstringThe execution.
step_idstringUnit key: step, step[i], compensate:<unit> or on_<kind>:<step> (a step of an on failure, on cancel or on timeout handler).
task_name, contract_versionstringThe contract to run.
inputanyEvaluated, validated input.
idempotency_keystringStable across retries and redeliveries of this unit.
attemptintegerAttempt number, starting at 1.
redeliverybooleantrue when the previous delivery of this same attempt had an unknown outcome.
deadline_ms, timeout_msinteger, optionalPresent when the step has a timeout.

Response 204 with an empty body: no task arrived within wait_ms.

Errors: 404 UNKNOWN_SESSION; 403 FORBIDDEN.

POST /v1/workers/unregister

Close a session. Tasks it still holds become uncertain and are redelivered at once.

Request: {"worker_session_id": "..."}. Response 200: {"accepted": true}. Errors: 404 UNKNOWN_SESSION; 403 FORBIDDEN.

POST /v1/tasks/complete

Report a successful result.

FieldTypeRequiredMeaning
tokenstringyesToken from the task.
outputanynoResult. null or absent means {}. Defaults are applied and it is validated against the task’s output contract.
{"token": "41415faa-040f-4e35-9b92-b973d701ec2b", "output": {"valid": true}}

Response 200:

BodyMeaning
{"accepted": true}The step succeeded with this output.
{"accepted": false, "reason": "output violates the task contract"}The attempt failed with OUTPUT_CONTRACT_VIOLATION (not retried).
{"accepted": false, "reason": "duplicate result; step already resolved"}The step was already resolved. Recorded as StaleResultIgnored.
{"accepted": false, "reason": "unknown token (execution finished or token invalid)"}The token is unknown, or its execution has finished.

A success is accepted for any token issued for a step that is not yet resolved, including the token of an earlier attempt that timed out or was considered lost. See Delivery guarantees.

In shared mode, a result for an execution that another server owns is stored in that server’s inbox and answered with {"accepted": true} once stored; the owner then records it as above (a contract violation as OUTPUT_CONTRACT_VIOLATION, a duplicate as StaleResultIgnored). A token whose execution has finished still gets unknown token. See Shared mode.

Errors: 400 INVALID_REQUEST (token is required); 403 FORBIDDEN when a per-worker identity reports for a token issued to another worker; 413 PAYLOAD_TOO_LARGE when the output is larger than --max-payload-bytes. A refused output is not recorded and the attempt stays in flight, so the worker can still report a smaller result or a failure:

{"error":{"code":"PAYLOAD_TOO_LARGE","message":"task result is 312 bytes; the limit is 200 (--max-payload-bytes or the workflow's `limits`)"}}

POST /v1/tasks/fail

Report a failed attempt.

FieldTypeRequiredDefaultMeaning
tokenstringyesToken from the task.
error_codestringyesApplication error code, matched against the step’s non_retryable list.
error_messagestringno""Human-readable message.
retryablebooleannotruefalse fails the step without further attempts.
detailsanynononeStructured details, stored with the error.
{"token": "3d932a10-600d-4989-acc7-5234a3183b06", "error_code": "CARD_DECLINED",
 "error_message": "card was declined", "retryable": false,
 "details": {"decline_code": "insufficient_funds"}}

Response 200: {"accepted": true}, or {"accepted": false, "reason": ...} with stale attempt (the token is not the attempt currently in flight), duplicate result; step already resolved, or unknown token (execution finished or token invalid).

Errors: 400 INVALID_REQUEST (error_code is required); 403 FORBIDDEN.

gRPC binding

With --grpc-addr, the worker protocol is also served over gRPC as orbit.worker.v1.WorkerService (RegisterWorker, Heartbeat, PollTask, CompleteTask, FailTask, UnregisterWorker), defined in protocol/orbit/worker/v1/worker.proto. Credentials go in the authorization: Bearer <token> metadata entry. Errors map to gRPC status codes as follows, and carry the ORBIT code in the orbit-error-code metadata entry and as a CODE: prefix of the message:

HTTP statusgRPC status
400INVALID_ARGUMENT
401UNAUTHENTICATED
403PERMISSION_DENIED
404NOT_FOUND
409FAILED_PRECONDITION
413 (PAYLOAD_TOO_LARGE, CompleteTask) and 429RESOURCE_EXHAUSTED
500INTERNAL
503 (NOT_LEADER, cluster standby)UNAVAILABLE

PollTask returns an ExecuteTaskRequest with an empty token where HTTP returns 204. Client identities cannot use the gRPC binding: every call gets PERMISSION_DENIED with the message FORBIDDEN: client `app` may not use the worker protocol. A result larger than --max-payload-bytes gets RESOURCE_EXHAUSTED with orbit-error-code: PAYLOAD_TOO_LARGE.

See also