Use the runtime API
For: application developers.
Call oz0.runtime.v1.RuntimeService on the edge's HTTPS port, 7443, to run agents and inspect their jobs. The API supports Connect with JSON or protobuf, gRPC and gRPC-Web. The CLI uses this same API, and an application can use an HTTP client, gRPC library or client SDK.
| Method | Does |
|---|---|
StartJob | Starts an agent. The job's ID is the start's key: the same start again answers with that job, running or ended, and any other start under the ID is refused. See job IDs and retries |
GetJob | Status, answer or error, child job IDs, and cost per branch |
CancelJob | Cancels a job and its children |
ListJobs | Recent top-level jobs, newest first |
WatchJob | A stream of what the job and its child jobs do as it happens: text, tool calls, tool results, children and approvals, ending with the job's final state. Each event has a seq and a resume_token; a watcher that reconnects with since_seq and the token gets only what came after, on any edge. See Live feeds |
OpenFeed, UpdateFeed | One stream for many conversations and jobs, and the person's own chats, that a browser tab or an app keeps open; UpdateFeed adds and removes what it follows, on any edge. See Live feeds |
ListApprovals | Lists approvals oldest first, for a job, job tree or accessible tenants. See filters and partial results. |
DecideApproval | Approves or denies one, with a comment; admins and operators may. See Approvals |
GetJobTrace | The job's steps from its history, with what each cost, and findings about what looks wrong. See Job history. For a job whose ID (or its tree's root's) starts with cov., also the decision points it reached (policy-path coverage) |
GetJobStep | What one step read and wrote; step job is what the job itself was started with and answered. It can hold customer data: admins and operators may, and each call is in the audit log |
GetCatalog | The tenant's agents and flows, with their input and output schemas |
ListStartable | Lists what StartJob would allow for this caller, with conversation filters. See startable catalog. |
GetJobBoard | The notes and findings the agents of a job's tree pinned to its shared board; their text and tags need the content permission, and a view of them is audited. See Colleagues and the board |
GetJobWait | Why a job waits, in plain words: its pending steps, their queues and attempts, the last failure, and what each waits for. See why is a job waiting? |
ListQueues | The tenant's task queues with their backlog, their oldest task and the nodes that poll them, and why the others do not. See queues |
SetJobTags | Adds tags to a job and removes others, even after it ended; operators and above may |
ExportJob | A job as one JSON document: the job, its trace, findings and tags, and with the content permission each step's input and output, which is audited |
CompareJobs | Two jobs side by side, their steps aligned. See compare two jobs |
RetryJob | Starts a job again with the same agent, input, tags and budget, under a new ID; operators and above may. With an Idempotency-Key header, a retry of the call answers with the job the first one started (retried) |
FireRoutine, ListRoutineFires, BackfillRoutine | Run a routine now (or as a dry run), list its fires, and run a schedule's fires for a past period. See routines |
ListEvents | The tenant's event sources, its latest events and the jobs that wait for one; an event's text needs the content permission. See waiting for events |
GetConversation, ListConversations | A conversation's latest turns (50, or limit up to 200), by its ID or one of its jobs, with what was said and answered for those with the content permission (audited); and the tenant's latest conversations, facts only. StartJob continues one with continue_from or conversation. Someone else's private chat needs reason. See conversations |
ListMyConversations, UpdateMyConversation, ShareConversation, UnshareConversation, ListConversationShares | A person's chats, for accounts only: their own and those shared with them, pinned first, with a cursor; pin, archive, rename, mark read, remove from the list (which erases nothing); the owner's shares. See chats |
Listing waiting approvals
The approvals list shows what waits for a person's decision across the tenant's running jobs, oldest first. job narrows it to one job; adding tree includes jobs it delegated to.
Across tenants, every_tenant or tenants (at most 20 named tenants) lists approvals in each tenant the caller may read. Each approval names its tenant. The answer reports tenants_read, tenants_refused where the caller lacks the required role, and tenants_unread for tenants beyond the first 20 by name or served by another region. One unread or refused tenant does not fail the whole request.
Listing startable agents and flows
The startable catalog lists agents and flows that StartJob would accept from the caller now. It applies start permissions or agent grants, key scopes, retirement and kill switches.
Entries say whether they take part in conversations and whose conversations they may continue. conversations_only narrows new-chat choices; continues narrows hand-over targets. A reader gets an empty list with the reason. The console's chat picker uses this catalog. See the chat drawer.
MCP, A2A and AG-UI
The same agents, for the same callers, are also an MCP server at POST /mcp/<tenant>, with tasks for long jobs, A2A agents with signed Agent Cards at /a2a/<tenant>/<agent>, and an AG-UI route at POST /v1/agui/agents/<agent> for apps built on an AG-UI client, where approvals and questions are the protocol's interrupts. All of them start jobs through StartJob, so its rules hold there too.
Typed HTTP routes and the OpenAPI document
Next to the Connect API, the edge has plain HTTP routes, for the same callers:
| Route | Does |
|---|---|
POST /v1/agents/<name>/jobs | Starts an agent or a flow with its input as the body: JSON, or text/plain for a text input. It answers 201 with the job. |
GET /v1/jobs/<id> | The job: its status, and its answer once it has one |
GET /v1/openapi.json | An OpenAPI 3.1 document of the tenant's agents and flows; ?plugin=<name> for one plugin's |
GET /v1/jobs/<id>/events | The job's events as Server-Sent Events, what WatchJob streams, resumed with Last-Event-ID |
POST /v1/feeds, GET /v1/feeds/<id>/events | A feed made for Server-Sent Events, and its events |
POST /v1/stream-tickets | A one-time ticket that opens one of those streams in a browser's EventSource, which cannot send a key |
The streams, their tickets and the tenant's allowed origins for pages elsewhere are on Live feeds. An API key in a URL is refused on them.
curl --cacert ca.crt -H "Authorization: Bearer $OZ0_API_KEY" -H 'Content-Type: application/json' \
-d '{"repo": "acme/api", "pr": 42}' 'https://edge.example.com:7443/v1/agents/acme-reviewer/jobs?tenant=acme'
curl --cacert ca.crt -H "Authorization: Bearer $OZ0_API_KEY" 'https://edge.example.com:7443/v1/openapi.json?tenant=acme' > oz0.json
The document has one operation per agent, run_<name>. Its request body is the agent's input schema, and its response is the job, with output typed by the agent's output schema. Generate a client from it with any OpenAPI generator. It changes when a plugin changes an agent or a schema, and its ETag says when. A plugin installed before Orchestrator Zero put schemas in plugins' bundles needs plugin update before its agents' schemas appear.
With thousands of agents the whole document is megabytes (17.6 MB for 10,000): ?plugin=<name>, and oz0 client generate --plugin <name>, describe one plugin's agents and flows, under an ETag that names the plugin.
Start a job with curl
curl --cacert ca.crt -H "Authorization: Bearer $OZ0_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"agent": "acme-reviewer", "input": {"repo": "acme/api", "pr": 42}, "timeout": "900s", "tenant": "acme"}' \
https://edge.example.com:7443/oz0.runtime.v1.RuntimeService/StartJob
{ "job": { "id": "job-bb270cf1-...", "agent": "acme-reviewer", "status": "JOB_STATUS_RUNNING", "startedAt": "..." } }
Then poll it:
curl --cacert ca.crt -H "Authorization: Bearer $OZ0_API_KEY" \
-H 'Content-Type: application/json' -d '{"id": "job-bb270cf1-...", "tenant": "acme"}' \
https://edge.example.com:7443/oz0.runtime.v1.RuntimeService/GetJob
Requests and responses
StartJob takes:
invalid_argument (HTTP 400) and what is wrong, and no job is made.default.Idempotency-Key header when the request has one). May not contain /, \, " or control characters. The same start again under it answers with that job and retried: true; another start under it is refused with already_exists. See job IDs and retries.provider, path, requestSha256, status, contentType, body). The edge answers the job's model calls, and its child jobs', from them in order, at no cost. It needs the tenant's admin role, and the job is marked as a replay. Leave id empty: such a job always gets a new ID of its own, runs for at most seven days, and while its recording is kept, a StartJob with that id is refused (FailedPrecondition). The test bench uses it.A Job has id, agent, status, startedAt, closedAt, output (the answer, text or JSON), error, children (direct child job IDs), approvals (what it waits for, while it runs), flow (true for a flow's job), recording (for a job started with recordedCalls: how many it had, how many were used, and what differed) and cost. cost holds the totals (usd, calls, inputTokens, outputTokens) and branches: one entry per job in the tree, the job itself first, with its agent, models, nodes, tokens, cached tokens and cost, and translated when its calls went through a translating provider.
StartJob answers with job and retried, which is true when the start was a retry and nothing started again.
Job IDs and retries
A start may reach the edge and be applied even when its answer is lost on the way back, so a client that retries must not start a second job. The job's ID is the start's idempotency key:
- The same start again under an ID, the same agent, input, tags, team, budget, timeout, data subjects,
conversation and other options, from the same caller, answers with the job of that ID as it is, running or ended,
with
retried: true. Nothing runs again, nothing is counted against a quota again, and nothing is audited as a start. - Another start under the ID (another input, agent, option or caller) is refused with
already_exists(HTTP 409) and a detail,oz0.runtime.v1.IdConflict, whosereasonisoz0_id_conflict, with the job's ID and, when you may read that job, its start time. The refusal comes before anything of the start happens: no quota, budget, turn or file hand-over. A job that ended never runs again under its ID: start a new job under a new ID. - Without an ID, send an
Idempotency-Keyheader (1 to 128 visible ASCII characters, such as a UUID) instead: the edge derives the job's ID from you and the key, so the same start with the same key is the same job, and the same key with another request is refused as above.oz0 runand the SDKs send one on every start without an ID and retryunavailablewith it. A start withrecordedCallstakes none: it always gets a new ID. - An ID the edge gave (a start without an ID or a key) is the job's alone: no start with that
idis a retry of it. - How long: an ID stays taken for as long as its job's history is kept, the tenant's retention. After that it is free again.
- Another caller's job: the caller is part of the request, so a start under someone else's job ID is refused, never joined.
RetryJob and FireRoutine take the Idempotency-Key header the same way: a retry of the call answers with the job
or fire the first call started (retried: true), and another request under the key is refused.
The REST start
(POST /v1/agents/{agent}/jobs) takes the header too, answers a retry with 200 instead of 201, and its JSON error
carries reason, job and started_at for a refused ID.
An AG-UI resume sent again with the same
run ID (or Idempotency-Key) answers the decisions its first try made as made, and decides what is left. The admin
API's calls take the same header: see the admin API.
The protobuf definition is in proto/oz0/runtime/v1/ in the repository: the service in runtime.proto, its messages in the runtime_*.proto files beside it.
Client SDKs
Typed clients for three languages are built from that file in the same change as the API, and carry the server's version: the Python SDK (oz0-client), the TypeScript SDK (@oz0/client, Node 24) and the Go SDK. Each starts a job and waits for it, watches it, answers its questions and decides its approvals, and reads the catalog and what jobs cost; every other method is there too, typed. For one tenant's agents with their own input and output types, generate wrappers from its OpenAPI document:
oz0 client generate --tenant acme --lang python --out src/oz0agents
--lang is python, ts or go. The command reads the document with the CLI context's operator certificate, or with an API key from OZ0_API_KEY when --url or OZ0_URL names the edge (--ca-file for its CA), or from a saved file with --from. Run again, it leaves a file generated from the same catalog alone, so an app's CI can run it on every build; a file from another version of the generator, or for another tenant or Go package, is written again.
From v1.0 the API only grows: make proto-breaking checks every change against the last release with buf breaking, so an SDK or a client you generated yourself keeps working with newer servers. For another language, generate a client from the protobuf file with buf or protoc.
Related tasks
- Runtime API authentication: Authenticate clients and apps acting for individual people.
Previous section links
- Section: admin keys
- Section: apps that act as their people
- Section: authentication
- Section: keys scoped to agents and flows
- Section: what a key creates is labelled with it