HTTP API
The HTTP API serves every operation of Auto Runtime. It covers playbooks, function and workflow definitions, runs and their history, and requests. It also covers events, analytics and the tool servers a playbook's functions may use. This page lists every route with its fields, limits and errors.
The MCP endpoints serve the same operations as tools. A definition's type is reasoning, interaction, prediction, recall, computation or workflow.
Requests and access
Send requests to the server's address, such as http://localhost:5555, with a credential.
- Org routes begin at
/v1/orgs/{org}. Playbook routes begin at/v1/orgs/{org}/playbooks/{playbook}. - Send the credential, an API key or a signed-in person's token, as
Authorization: Bearer <key>. Keep credentials out of prompts and source documents. - A server in local mode asks for no credential. The Sign-in and token reference gives the rules the server applies to credentials.
- Send command bodies as UTF-8 JSON with
Content-Type: application/json, at most 1 MiB. Compressed bodies are not accepted. - Query parameters are for
GETandDELETErequests, which take no body. A field may be supplied in only one place.
Each API key belongs to one org and has permissions for a set of playbooks:
| Permission | Lets the caller |
|---|---|
org:read | Read the org's playbooks and connections, list models and search the org's tool servers |
org:write | Create, update and retire playbooks, and add and remove connections |
playbook:read | Read definitions, runs, history, events, requests and analytics, and search and read tools |
playbook:write | Create, change, retire or run a function or workflow, cancel a run, send or publish an event, answer a request and call a tool |
Each route table below names the permission a route needs.
When the server sends telemetry, every response except that of /health carries its trace id in the x-trace-id header. A request with a W3C traceparent header continues the caller's trace.
The OpenAPI document, the guides and the API catalog
| Method and route | Returns |
|---|---|
GET /v1/openapi.json | The OpenAPI 3.1.1 document of this API |
GET /v1/guides/{guide} | One guide, as text/markdown |
GET /.well-known/api-catalog | The API catalog of this server, as application/linkset+json |
GET /health | Whether the server is up |
These four need no key. Where sign-in is on, the protected resource metadata needs no key either. It is served at /.well-known/oauth-protected-resource and under the path of each MCP endpoint.
The OpenAPI document
The server builds the document when it starts, from its operations: the same operations its routes and its MCP tools serve.
- Each operation appears once. Its
operationIdis the operation's name, withsearch_in_orgfor the org'ssearch. Its summary is its title. Its tag names the resource it acts on. - Its description is its MCP tool's description. An operation served only over HTTP has no tool, so its description is its own: the five prediction routes, the three routes of Studio's connection page,
get_playbook,get_connection,get_run_history,get_playbook_analyticsandvalidate. - Its input is in the path, and in the query string of a
GET. A command, or a query routed byPOSTsuch asvalidateanddry_run, takes it in the JSON body. The two uploads take a CSV dataset or a candidate's bytes as the body, and the document gives their maximum size. - Its schemas are JSON Schema 2020-12. Each has its own
$idunderhttps://on.auto/schemas/runtime/, with its definitions inside it. - Each problem an operation can return is listed under its status.
- The server URL is the relative
/v1, so the document stays valid behind any host.
A client library can be generated from the document, and a gateway that accepts OpenAPI documents can add the runtime from it. Each release's document is kept in the repository at docs/assets/openapi.json, so a client library can be generated without running a server.
The guides
GET /v1/guides/{guide} returns one of the guides that the MCP endpoints serve as guide://{guide}, with the same text. The guides say what the words mean, give the format of each definition type, and hold the recipes for common tasks. The OpenAPI document lists their names. Each guide has its own path. So the server treats an unknown guide name like any other path it does not serve: without a key, the response is 401.
The API catalog
GET /.well-known/api-catalog returns a linkset in the API catalog profile. It lists the API at /v1, described by the OpenAPI document and documented by each guide with its title, and the API's status at /health.
- Every address is absolute. It is based on
PUBLIC_URLwhen that is set, and otherwise on the origin the request arrived at. - Behind a proxy, that origin is the scheme and host the server itself receives. The server reads no
X-Forwarded-*header. - Responses for the catalog,
HEADincluded, carryLink: <…/.well-known/api-catalog>; rel="api-catalog".GET /healthcarries the same relation, so a client given only the health address can find the catalog, and from there the document and the guides.
Caching
The document, the guides and the catalog change only when a new release of the server is deployed or the server is configured again.
- Each carries a strong
ETagof its bytes andCache-Control: public, max-age=300. - Each responds with
304and no body when the request'sIf-None-Matchnames its tag, a list that includes it, or*. HEADreturns their headers without the body./healthis never cached: it responds withCache-Control: no-store.
Playbooks
These routes are relative to /v1/orgs/{org}:
| Operation | Method and route | Permission | Input |
|---|---|---|---|
create_playbook | POST /playbooks | org:write | playbook, name, optional description |
list_playbooks | GET /playbooks | org:read or playbook:read | Optional include_retired |
get_playbook | GET /playbooks/{playbook} | org:read | Playbook id in path |
update_playbook | PUT /playbooks/{playbook} | org:write | name and description |
retire_playbook | POST /playbooks/{playbook}/retire | org:write | Playbook id in path |
| Field | Rule |
|---|---|
playbook | The id: 3 to 48 lowercase letters, digits and hyphens, beginning with a letter |
name | 1 to 100 characters, stored without the whitespace around it; it cannot be all whitespace |
description | At most 2,000 characters, stored without the whitespace around it |
Each operation returns the playbook, with its id, name, description, status (active or retired), created_at, created_by, updated_at and, once retired, retired_at. list_playbooks returns them in playbooks.
list_playbookslists the playbooks the caller may access. It leaves out retired ones unlessinclude_retiredasks for them. It also accepts a key with onlyplaybook:read, so a key that may only read inside some playbooks can find them.- A key limited to a list of playbooks can create only a playbook whose id is on that list.
update_playbookrequires bothnameanddescription, even when only one of them changes. An update that changes nothing records nothing.- Retirement is permanent. A playbook id cannot be reused, and a retired playbook cannot be restored.
- A retired playbook stays readable:
get_playbookreads it,include_retiredlists it, and its definitions, runs and history remain readable. Commands inside it returnconflictwith the kindretired. Retiring it again succeeds and changes nothing.
| Case | Response |
|---|---|
create_playbook with an id the org has or had | conflict |
update_playbook of a retired playbook | conflict |
get_playbook, update_playbook or retire_playbook of an id the org does not have | not_found |
| A change that meets another change to the org's playbooks at the same moment | conflict; send it again |
Models
| Operation | Method and route | Permission | Input |
|---|---|---|---|
list_models | GET /v1/orgs/{org}/models | org:read | Optional provider |
provider is a provider prefix of 1 to 32 lowercase letters, digits or hyphens, starting with a letter. The result is the same over HTTP and MCP; Model information describes its fields, aliases, wildcard entries and incomplete lists.
Reasoning functions
These routes are relative to /v1/orgs/{org}/playbooks/{playbook}:
| Operation | Method and route | Permission | Input |
|---|---|---|---|
create_definition | POST /definitions/reasoning | playbook:write | name, source |
list_definitions | GET /definitions/reasoning | playbook:read | Optional include_retired |
get_definition | GET /definitions/reasoning/{name} | playbook:read | Name in path |
update_definition | PUT /definitions/reasoning/{name} | playbook:write | source |
retire_definition | POST /definitions/reasoning/{name}/retire | playbook:write | Name in path |
run_definition | POST /definitions/reasoning/{name}/run | playbook:write | Optional input, optional UUID run_id |
get_run | GET /runs/{run_id} | playbook:read | Run id in path |
The source is a reasoning function document. These rules apply to the definitions of every type:
- A name follows the playbook id rule: 3 to 48 lowercase letters, digits and hyphens, beginning with a letter.
- A name is unique within its type and playbook, and cannot be reused after retirement.
- A source document may be at most 65,536 UTF-8 bytes.
- Changing a document creates a version. Updating it with identical source records no change.
- A run uses the latest active version.
- A retired definition can be read, but cannot be edited or run.
- A definition carries its
input_schemaandoutput_schemawhere its document declares them. A reasoning function, a computation function or a workflow whoseoutput.shapenames an output shape has the shape's schema as itsoutput_schema, and the shape's name asdetails.shape, such as"details": { "shape": "series" };detailsis left out otherwise. A prediction function has theseriesshape so, since its questions answer a series.list_definitionsshows both too.
create_definition, update_definition, get_definition and retire_definition return the definition with its overview; list_definitions leaves it out. The overview describes the definition in plain words, for a person who does not read its document:
- It is Markdown with headings, lists, tables of its fields and, where one helps, a diagram. It says what the definition needs, what it produces and what it does, as its type has it. For a reasoning function, it also gives the model, the tools and what the model is told.
- The runtime writes it from the document when it saves the version.
- It ends with Used by, a line for each active definition of the playbook that uses this one: a workflow whose
run_definitioncalls it, a prediction function that sends it builds to train or asks it to approve a version, or a recall function with a filter on its runs (by theirsubject) or on its changes (by theirsource). - Used by is read each time the definition is returned, so it is as current as the playbook. It is left out when nothing uses the definition.
Interaction functions
Interaction functions use the operations of reasoning functions, with interaction in place of reasoning in each route, such as POST /definitions/interaction and POST /definitions/interaction/{name}/run. The source is an interaction function document. Names, document size, versions and retirement follow the rules for reasoning functions.
A run responds with 200, status: started and its run_id. It then waits until its request is answered, expires or is cancelled. Two kinds of run finish within the request instead:
- a notification to the inbox;
- a function that calls a tool, whose output is the tool's result, or which ends in one of the endings its format lists.
| Case | Response |
|---|---|
A to or an argument the function cannot render | conflict with the kind unworkable |
| A server or a tool the playbook may not use | unavailable with the kind tool_not_offered |
| The playbook has as many open requests as the runtime allows | unavailable with the kind requests_full |
The requests of a playbook have two routes of their own, relative to /v1/orgs/{org}/playbooks/{playbook}:
| Operation | Method and route | Permission | Input |
|---|---|---|---|
list_interactions | GET /interactions | playbook:read | Optional to, type, function, limit and cursor |
answer_interaction | POST /runs/{run_id}/answer | playbook:write | Run id in path; answer and an optional claimed_for |
Listing open requests
list_interactions returns interactions, the playbook's open requests newest first, with has_more and next_cursor. They are the requests of interaction functions and of prediction functions' builds. An answered request leaves the list. Anyone who may read the playbook sees each request's party and message, just as they see a run's input.
tokeeps only the requests to one party.typekeeps only those from one type of function.functionkeeps only those from functions of one name. A name is unique within its type, sofunctionwithtypenames one function.
Each request has these fields:
| Field | Contents |
|---|---|
run_id | The run that asked, which answer_interaction takes |
type | The type of the function that asked |
function | The name of the function that asked |
version | The version of the function that asked |
to | The party the request goes to |
delivery | The server and tool the request is sent through, or null for the inbox |
message | The message of the request |
takes_answer | true for a question, false for a notification |
answer_schema | The JSON Schema an answer must match, as the request recorded it when it was asked; null for a notification |
requested_at | When the request was made, in ISO 8601 UTC |
expires_at | When the request expires unanswered, in ISO 8601 UTC |
attempts | The delivery attempts made so far |
conversation | The conversation the playbook reads replies in, or null |
answerer | The party whose reply counts, or null |
reply_refusals | How many replies from the answerer the playbook refused |
standing | in_inbox, to_deliver, delivering, delivered, retrying, undelivered, or cancelling once a cancel of its run was asked |
answer_interaction checks the answer_schema the request recorded. That holds even after the function has a newer version, whose output_schema is the one get_definition shows. A schema may be up to 64 KiB as JSON, so a page of 100 requests can contain up to 6.25 MiB of schemas. A smaller limit keeps a page smaller.
Answering a request
answer_interaction takes the run's run_id, an answer and an optional claimed_for:
answermust match the request's answer schema, and may be at most 64 KiB as JSON.claimed_fornames whom the caller says it is answering for, in at most 256 bytes. The runtime never checks it.
It records the answer on the run as one fact, request_answered, and returns that fact:
| Field | Contents |
|---|---|
run_id | The run that asked |
answer_id | The id of the fact |
answer | The answer |
claimed_for | Whom the caller says it is answering for, when given |
answered_by | The caller's id; for a run answered by a reply, the playbook |
reply | For a run answered by a reply, that reply's identity |
answered_at | When the answer was recorded |
The run takes the answer within about a second (the server checks waiting runs every second by default), as Answering a request explains.
- A run of an interaction function succeeds with the answer as its
output, which reaches a workflow that waits for it. - A prediction function's build scores the candidates the answer names,
{ candidates }, each by the SHA-256 its upload returned. A candidate not uploaded to that build is refused asinvalid_input.
| Case | Response |
|---|---|
| An answer that does not match | invalid_input with pointers under /answer; the request stays open |
| The same answer again | The fact the run took |
| A different answer, an answer to a request that has ended or whose run was asked to cancel, or an answer to a notification | conflict |
| A run the playbook does not have | not_found |
A request nobody answers
When nobody answers a request before it expires, its run ends rejected, with the reason unanswered and the kind expired. A notification that could not be delivered ends with the kind undelivered. A request with the same run id and input then returns that ending as a 410 problem of the type https://on.auto/problems/unanswered. How a run ends lists every ending.
Computation functions
Computation functions use the operations of reasoning functions, with computation in place of reasoning in each route, such as POST /definitions/computation and POST /definitions/computation/{name}/run. The source is a computation function document. Names, document size, versions and retirement follow the rules for reasoning functions.
A run finishes within the request that starts it. Saving a definition checks its program, and refuses each problem under /source with its line.
| Case | Response |
|---|---|
| A ready checker does not finish the check within 2 seconds of the server's wall-clock time | invalid_input; nothing is saved |
| A save finds no checker ready | unavailable (503); the same call may be tried again |
| The program cannot produce its output for the input | conflict with the kind unworkable |
A program cannot produce its output when it:
- raised an error;
- returned a value JSON cannot represent, or an output larger than a run may record (see Runs and results);
- did more work or used more memory than a run allows.
The same input gives the same unworkable result again, unless the program reads the clock. Change the definition or the input rather than retrying.
Recall functions
Recall functions use the operations of reasoning functions, with recall in place of reasoning in each route, such as POST /definitions/recall and POST /definitions/recall/{name}/run. The source is a recall function document. Names, document size, versions and retirement follow the rules for reasoning functions.
A recall function keeps a view: a JSON value its module builds from the playbook's events, one event at a time. A run computes its answer from the view as it stands.
- A playbook may have at most 32 active recall functions. Saving one more returns
conflict. - A run finishes within the request that starts it.
get_definitionadds the view'sstanding.get_runadds to the run'srecordthe checkpoint the run read at, which says how far into the playbook's history the view had read. See How the view is kept.
| Case | Response |
|---|---|
| The view of the latest version is still being built | unavailable with the kind rebuilding and a Retry-After |
| The view has stalled | conflict with the kind stalled |
The module's answer cannot produce an output | conflict with the kind unworkable |
Prediction functions
Prediction functions use the operations of reasoning functions, with prediction in place of reasoning in each route, such as POST /definitions/prediction and POST /definitions/prediction/{name}/run. The source is a prediction function document. Names, document size, versions and retirement follow the rules for reasoning functions.
- A playbook may have at most 32 active prediction functions.
- A question finishes within the request that asks it, and the function's active version makes the prediction. A question such as
{ "series": "north", "horizon": 1 }answers a series whoseobservedline holds the last periods of the dataset and whosepredictedline holds the period asked, with itsvalue,lowandhigh; withouthorizonit holds every horizon the function names. The run'srecordholds the model'sversionand thecoverage. The input and the output has every field, and A question an answer in full. - A build (the input
{ build: {} }) responds with200andstatus: started, then waits for its trainer, as A build explains. get_definitionadds the dataset's and the models'standing.
Five routes are served only over HTTP; no MCP endpoint offers them as tools. They are relative to /v1/orgs/{org}/playbooks/{playbook}:
| Operation | Method and route | Permission | Body | Returns |
|---|---|---|---|---|
upload_dataset | PUT /datasets/{sha256} | playbook:write | text/csv with Content-Length, at most 64 MiB | { dataset, bytes, rows, series, columns, period, ... } |
read_build_brief | GET /runs/{run_id}/build/brief | playbook:read | None | The brief, text/markdown |
read_build_rows | GET /runs/{run_id}/build/rows | playbook:read | None | The training rows, text/csv, streamed |
upload_candidate | POST /runs/{run_id}/build/candidates | playbook:write | An ONNX model, application/octet-stream with Content-Length, at most 64 MiB | { candidate, bytes, opset } |
activate_version | POST /definitions/prediction/{name}/activate | playbook:write | { version } | { function, version, rule, activated_at } |
- A body sent without its
Content-Lengthgets411. A body whoseContent-Lengthis larger than the route accepts gets413, before the body is read. - The build itself is answered through
answer_interaction, with{ candidates }: the SHA-256 of each candidate, asupload_candidatereturned it.
The three build routes work only on a build that is waiting for its answer:
| Case | Response |
|---|---|
| A run the playbook does not have | not_found |
| A run that is not a build | conflict with the kind not_a_build |
| A build that took its answer or ended | conflict with the kind build_ended |
upload_candidate refuses a candidate as invalid_input when:
- the runtime cannot load it;
- its signature does not match what the function feeds it, or its opset is older than 13;
- the build already has the 8 candidates it accepts.
While every model process available to builds is busy, it returns unavailable with the kind model_process_busy and a Retry-After. The check shares those processes with the builds' scoring, and never takes one a question needs.
activate_version returns not_found for a version the function did not build, and conflict with the kind already_active for the version that is already active.
Workflows
These routes are relative to /v1/orgs/{org}/playbooks/{playbook}:
| Operation | Method and route | Permission | Input |
|---|---|---|---|
create_definition | POST /definitions/workflow | playbook:write | name, source |
list_definitions | GET /definitions/workflow | playbook:read | Optional include_retired |
get_definition | GET /definitions/workflow/{name} | playbook:read | Name in path |
update_definition | PUT /definitions/workflow/{name} | playbook:write | source |
retire_definition | POST /definitions/workflow/{name}/retire | playbook:write | Name in path |
run_definition | POST /definitions/workflow/{name}/run | playbook:write | Optional input, optional UUID run_id |
get_run | GET /runs/{run_id} | playbook:read | Run id in path |
cancel_run | POST /runs/{run_id}/cancel | playbook:write | Optional reason |
send_run_event | POST /runs/{run_id}/events | playbook:write | event with type, and optional id, source, subject and data |
A saved workflow
The source is a workflow document. Names, document size, versions and retirement follow the rules for reasoning functions.
- A saved workflow has the
media_typeapplication/yaml. - Its
description,input_schemaandoutput_schemacome from the document, theoutput_schemafrom itsoutput.shapewhere it names one, withdetails.shape. - A workflow whose document has a
schedulealso hastriggers, inget_definitionandlist_definitions. They are the triggers its schedule names, in the order it names them.
| Trigger field | Contents |
|---|---|
kind | event, cron or every |
reference | /schedule/on, /schedule/cron or /schedule/every |
filters | For an event trigger: its filters, each with its reference, type and attributes |
expression | For a cron trigger: its expression |
milliseconds | For an every trigger: its period |
get_definition of an active workflow with triggers adds triggers_since: the id of the record that saved its current version. From that record on, every run its triggers start is a run of this version. triggers_since is one id for the whole workflow. A trigger that a later version left unchanged continues from the record that first saved it in that form, which triggers_since does not show.
Running a workflow
run_definitionreturns200as soon as the run begins, with itsrun_idandstatus: started. A run that ended before its first wait returns how it ended.- Read the run with
get_rununtil its status issucceeded,rejectedorfailed. - Read its steps with
get_run_history, which contains oneworkflow_input_appliedevent for each input the run took (see Run history and playbook events). - While the runtime is stopping,
run_definitionreturnsunavailable; try again shortly.
A workflow runs once for each run_id. Retrying under a run id says what a request with the id of an earlier run returns.
Sending an event to a run
send_run_event delivers an event to a run that is still started. It returns the run_id and the delivered event, with these filled in:
id, which the runtime makes when you leave it out;source, which is/callers/and your caller id when you leave it out;time, when it was sent.
| Part of the event | Limit |
|---|---|
| The whole event | 256 KiB as JSON |
type, id | 256 characters each |
source | A URI reference such as /ledger/eu, as for a published event; 1,024 characters |
subject | 1,024 characters |
data | Nests at most 510 levels deep, so that the run can keep the whole event in a list |
A run accepts an event with a given id only once, so a request can be retried with the same id.
| Case | Response |
|---|---|
| A type or source the runtime reserves for its own facts, as listed under Publishing events | invalid_input |
Text a published event may not contain: a control character, a lone surrogate or a noncharacter, or a type, id or subject with no character other than a space | invalid_input |
| The playbook has no running workflow with that run id, including one that has ended | not_found |
| The run cannot take the event at that moment, such as while the runtime is stopping | unavailable; try again shortly |
Cancelling a run
cancel_run needs playbook:write. It cancels:
- a workflow run that is still
started; - the run of an interaction function whose request waits;
- a prediction function's build, whether it is starting, waiting for its trainer or scoring.
It takes an optional reason, 1 to 1,024 characters with no control character. The run keeps it as the detail of its ending; without one, the detail says who asked.
- The request is recorded on the run at once, whichever server takes it.
cancel_runreturns200with the run as it stands, stillstarted. - Within a moment the run stops its tasks and cancels each run it waits for.
- It ends
rejectedwith the reasoncancelledand the kindrequested. Read it withget_run.
Asking again before the run has ended records nothing more.
| Case | Response |
|---|---|
| The playbook has no run with that id | not_found |
| The run has ended | conflict |
| The run ends within the request that started it | conflict |
These runs end within the request that started them, so they cannot be interrupted from outside:
- the run of a reasoning, computation or recall function;
- the run of an interaction function that calls a tool;
- a prediction function's question.
A cancelled run's rejection names the kind of its cancellation:
| Kind | The run was cancelled because |
|---|---|
requested | someone allowed to change the playbook asked, with cancel_run |
deadline | the workflow that waited for it ran out of time |
overrun | it ran as long as a workflow may run |
parent_ended | the workflow that waited for it ended first, or the branch that waited for it lost a race |
Runs and results
A run records its run_id, type, name, definition_version, status, timestamps and caller identity. A successful run includes output, and a rejected run includes a rejection. get_run also returns the detailed record. A run whose output takes an output shape says it as output_shape, such as "output_shape": "series" on a question to a prediction function; a build of a prediction function, and a run of a definition that declares no shape, has none.
Most runs finish within the request that starts them. These runs finish later, so run_definition returns them started, unless one ended before its first wait:
- a workflow's run, whose
recordis empty while it is in progress; - an interaction function's run that asks a person, or that sends a notification through a tool;
- a prediction function's build, the input
{ build: {} }, which waits for its trainer.
Read such a run again with get_run until its status is succeeded, rejected or failed. A cancelled run is rejected with the reason cancelled. Workflows and runs explains how a run waits and ends.
| What | Limit |
|---|---|
| Input | 256 KiB as encoded JSON, nesting at most 512 levels deep, as deep as a workflow can hold a value. A deeper one returns invalid_input at /input |
| Output and record together | 1 MiB (1,048,576 bytes) as JSON |
These limits apply independently of the request-body limit.
A run's output and its record share that 1 MiB. Each type of definition makes room for its record in its own way, so the largest output differs by type:
| Type | Largest output |
|---|---|
| Reasoning | What leaves room for the record. The record keeps less of the prompt to fit; an answer that leaves too little room ends conflict, unworkable |
Interaction with call | 1 MiB less the run's record, measured exactly |
| Computation | 1,048,320 bytes: the function keeps 256 bytes for its record |
| Recall | 1,046,528 bytes: the function keeps 2,048 bytes for its record, which also says where the view had read to |
| Workflow | 1,048,574 bytes; a larger output ends the run conflict with the kind oversized |
A function's output past its bound ends the run conflict with the kind unworkable. Each format page repeats the bound for its type.
Retrying under a run id
run_definition takes an optional run_id, a UUID. Supply one to retry a request without doing the work twice, or to read the run later. The runtime keeps one run under each id.
A request that reuses an id for another definition or another input returns conflict. A request with the same definition and input gets what the run under that id recorded:
| The run under that id | A request with the same id and input |
|---|---|
| Succeeded | Returns the recorded output; nothing runs again |
Was rejected as invalid_input | Returns the same rejection |
| Was cancelled | Returns its cancellation, cancelled (409) |
| Went unanswered | Returns unanswered (410), as A request nobody answers says |
| Still waits: a workflow run, an interaction function's request, or a prediction function's build | Returns the run as it stands, still started, and starts nothing |
Has no final result: it ended unavailable, conflict or failed, or it is started without waiting, such as after an interruption | Runs the definition again, as a new attempt. The attempt may use the latest version, and records the one it used |
The last row has two exceptions.
A workflow runs once for its id. A request with the id of a workflow run that ended without a final result returns conflict with the kind unworkable. Its detail says to use a new run id.
A run that may have called a tool is never attempted again under its id. That is a run of a reasoning function with tools, or of an interaction function with call:
- A run that recorded a tool call and has no final result returns
conflictwith the kindtools_called. When the run has ended and every tool it called is one its server marks read-only, the rejection addsbecause: "only_read", and its words say nothing was changed. - A run of such a function that is still
startedreturnstools_calledtoo, even before its first recorded call, since it may still be in progress. - A run that ended before its first call, for example with
tool_not_offeredortool_server_failed, runs again like any other.
After tools_called, read what the run called with get_run_history, and check its effects outside the playbook. Then start a new run under a new id, which calls the tools again. When a tool-calling run cannot finish gives the endings that come before it, tools_unfinished and effect_unknown.
A workflow step that calls a function meets the same rules when the server performs the step again, for example after a restart. A function run that may have called a tool then raises tools_called in the workflow; see When the function's run does not succeed.
The runtime records one final result for each run id. That does not mean an external effect happened only once.
Publishing events
publish_event records an event in a playbook, such as a month closed in a ledger or a deal won in a CRM.
| Operation | Method and route | Permission | Input |
|---|---|---|---|
publish_event | POST /v1/orgs/{org}/playbooks/{playbook}/events | playbook:write | event |
The body contains event, a CloudEvents 1.0 event:
| Attribute | Required | Contents | Limit |
|---|---|---|---|
source | Yes | Where the event comes from, a URI reference such as /ledger/eu or https://acme.example/ledger | 1,024 characters |
type | Yes | What happened, such as com.acme.ledger.month-closed | 256 characters |
id | No | Unique among the events of its source; the runtime makes one when you leave it out | 256 characters |
time | No | When it happened, in RFC 3339 such as 2026-10-01T09:00:00Z; when it is left out, the runtime uses the time it records the event | |
subject | No | What the event is about within its source | 1,024 characters |
specversion | No | 1.0 | |
datacontenttype, dataschema | No | The media type of data, such as application/json, and the absolute URI of a schema it follows | |
data | No | Any JSON value | Nests at most 510 levels |
- The event, with its id and time filled in, may be at most 240 KiB as JSON.
datanests at most 510 levels deep, so that a run can keep the event in a list. A deeper one returnsinvalid_inputat/event/data.- Any other attribute is an extension, and an event may have at most 32. Its name is 1 to 20 lowercase letters and digits. Its value is a string, a boolean or an integer, and it is kept as given.
As CloudEvents requires:
- no string may contain a control character, a lone surrogate or a noncharacter;
id,typeandsubjectneed a character that is not a space;datacontenttypeis a media type;- a
timehas a second of 60 only at the end of a day in UTC.
POST /v1/orgs/acme/playbooks/finance/events
Content-Type: application/json
{
"event": {
"source": "/ledger/eu",
"type": "com.acme.ledger.month-closed",
"id": "2026-09",
"subject": "september",
"data": { "region": "eu", "revenue": 120000 }
}
}It returns 200 with the event's id and time, and recorded_at, when the playbook recorded it:
{ "id": "2026-09", "time": "2026-10-01T09:00:00.000Z", "recorded_at": "2026-10-01T09:00:00.000Z" }list_playbook_events shows each published event as an event_published event. A published event starts the workflows whose event trigger matches it, and reaches the runs listening for its type.
Publishing the same event again
A playbook keeps one event for each source and id, so a request can be retried with the same id.
- Publishing the same event again records nothing, and returns the first
id,timeandrecorded_at. - A retry that leaves out
timeis the same event. - A retry that gives a
timeto an event first published without one is the same event too. It returns the time the runtime filled in the first time, not the one the retry gave. - Times are compared as instants, so
2026-10-01T10:59:00+02:00is the same time as2026-10-01T08:59:00Z. - A leap second counts as the first second of the next day, so
2016-12-31T23:59:60Zis the same time as2017-01-01T00:00:00Z. - A different event with the same
sourceandidreturnsconflict. - Without an
id, every request records a new event.
What the runtime keeps for itself
Some types, sources and attributes are reserved for what the runtime records itself. An event that uses one returns invalid_input:
| Reserved | Names | Refused at |
|---|---|---|
| Types | Every type the playbook's events show and every fact the playbook's capabilities declare, listed below | /event/type |
| Sources | Those beginning /runs/, /definitions/ or /callers/ | /event/source |
| Attributes | causationid, correlationid, caller, definitionversion, depth, calldepth, calledby, triggerkind and triggerreference | /event/ and the attribute's name |
The reserved types are run_started, run_deferred, run_succeeded, run_rejected, run_failed, run_cancel_requested, tool_call_started, tool_call_answered, tool_call_failed, request_answered, delivery_started, delivery_succeeded, delivery_failed, delivery_refused, reply_refused, telling_started, telling_succeeded, telling_failed, replies_read, reading_failed, interaction_requested, dataset_assembled, candidate_uploaded, candidate_refused, candidate_scored, candidate_disqualified, baseline_scored, baseline_not_walked, version_built, shadow_predicted, shadow_graded, shadow_passed, shadow_failed, version_activated, activation_refused, build_asked, build_ended, definition_created, definition_updated, definition_retired, event_published, workflow_input_applied, step_started, step_waiting, step_finished, step_failed, step_skipped and reaction_refused.
The reserved attributes are the ones the playbook gives its own events; see the playbook's events. So a published event cannot claim a cause, a caller or a depth it does not have.
Run history and playbook events
These routes are relative to /v1/orgs/{org}/playbooks/{playbook}:
| Operation | Method and route | Permission | Input |
|---|---|---|---|
list_runs | GET /runs | playbook:read | Optional type, name, status, limit and cursor |
get_run_history | GET /runs/{run_id}/history | playbook:read | Run id in path; optional order, limit and cursor |
list_playbook_events | GET /events | playbook:read | Optional type, since, run_id, order, limit and cursor |
get_event | GET /events/{event_id} | playbook:read | Event id in path |
A retired playbook stays readable: these reads work on it, while every change to it is refused with conflict. The playbook's own creation, changes and retirement are not playbook events; get_playbook shows them.
Listing runs
list_runs returns runs, newest first by when each run first started.
- A listed run has the fields
get_runreturns, withoutoutput,recordand the detail and issues of a rejection. - A rejection shows its
reason, withkindandbecausewhen the function gave them. statuskeeps only the runs whose status isstarted,succeeded,rejectedorfailed.typeandnamekeep only the runs of one definition.
A run's history
get_run_history returns the events of one run in the order the playbook recorded them, oldest first unless order is desc. Each event comes after the event that caused it. The events are the facts the runtime recorded about the run: each start and how it ended. A run that does not exist in the playbook returns not_found.
Runs that use tools (a reasoning function's, and an interaction function's that calls a tool) also record each call:
tool_call_started, withread_only: truewhen the server marks the tool read-only, andconnection_idbesideserverwhen the server is a connection a person added;- then
tool_call_answeredwhen the tool answered, withis_errorwhen its result is an error of its own; - or
tool_call_failedwhen it never answered, withbecause:arguments_refused,server_failure,timed_out,cancelled, ortoo_largewhen the tool's result was larger than one call may return.
The run of an interaction function records its request and its deliveries. Each delivery attempt starts with delivery_started and ends with delivery_succeeded, delivery_failed or delivery_refused.
| Event | Shows |
|---|---|
interaction_requested | The delivery, the party, the size of the message and the expiry |
delivery_started | The tool, the connection's connection_id where its server is one, and the size and digest of its arguments |
delivery_succeeded | Where the request landed and where its replies are read |
delivery_failed | because, including too_large for a result larger than one call may return, and retry_after_ms when the server asked for a delay |
delivery_refused | because, too_large or unworkable |
request_answered | The answer the run took, claimed_for and, for a reply, its identity |
reply_refused | Each refused reply's identity, never its words |
Each of the three endings of an attempt also shows the size and digest of the tool's result, when there was one. That result is the run's output, which its run_succeeded shows.
A workflow's history also contains one workflow_input_applied event for each input its run took. Each is followed by one event for each step that input moved, named for the state the input left the step in:
step_waiting,step_finished,step_failedorstep_skipped;step_startedfor a step that runs others, such as adoor afork, and was still running.
A step that starts and finishes in one input shows only step_finished.
The playbook's events
list_playbook_events returns the events of the whole playbook, newest first unless order is asc. They include:
- definitions created, updated and retired;
- runs started and ended;
- tool calls, and tests of a tool;
- the playbook's reads of a conversation,
replies_readorreading_failed; - its tellings,
telling_startedand thentelling_succeededortelling_failed; - the inputs workflow runs took;
- the events published to the playbook.
| Filter | Keeps |
|---|---|
type | One event type |
since | What the playbook recorded from that time on, in either order |
run_id | What one run and every run it started recorded, such as the functions a workflow called: its whole tree |
since is an ISO 8601 time with its offset, such as 2026-10-05T09:00:00Z. Its date must exist in the calendar and its hour must run from 00 to 23, so T24:00:00Z is refused.
A run that another run started belongs to the tree of the run at its top, so filtering by its own id returns no events. Read the tree from the id of the run you started.
Tool-call content
Tool-call events identify the server, the tool, the sizes of the arguments and the result, and how each call ended. The playbook keeps the full arguments and result of every call, scrubbed of secrets, unless the operator turns record_content off for the server.
- The sizes are of the scrubbed text the playbook keeps.
- A kept text's SHA-256 digest,
arguments_sha256orresult_sha256, is the key it is kept under. It is present only when the text was kept. - The history shows
argumentsandanswerwhen each is at most 2 KiB, and otherwise their sizes.get_eventreads one event with both in full. - Anyone with read access to the playbook can read that history.
A started call without an answer may have had an external effect; absence of an answer does not prove it was cancelled before acting.
Event fields
Each event has these fields, and no others:
| Field | Contents |
|---|---|
id | The event's id, a UUID that stays the same on every read; a step's is its input's id, an underscore and a number |
type | What happened, such as run_started, delivery_failed or step_finished |
summary | A sentence in plain language |
data | The fact itself, at most 4 KiB as JSON |
metadata | Where the event is recorded, when, who acted, and which definition, run and chain it belongs to |
metadata always has the first seven fields below, and each other field only when the event has it:
| Field | Contents |
|---|---|
stream | The stream the event is recorded in, such as playbook/acme/finance/runs/<run id> |
position | The event's number in its stream, from 1 |
global_position | The store's sequence across every playbook, with gaps; it is not the order a page reads events in |
correlation_id | The id of the run at the top of the chain the event belongs to, or null |
causation_id | The id of the event that directly led to this one, or null when nothing recorded did, such as for a run you started |
at | When it happened, by its own clock, in ISO 8601 UTC |
by | Who acted: a caller's id, playbook: and the playbook's id for the playbook's own work, or runtime for what the runtime records of its own accord, such as a connection whose credential it found it cannot read |
run_id | The run the event belongs to, when it belongs to one |
definition | The type, name and, when the event names one, version of the definition the run carries out or the event is about |
called_by | On a run a workflow's call started: the calling run's run_id, the reference of the call in its document, and run, which run of the call it is |
call_depth | On a run a call started: how many calls are above it |
depth | On a run a trigger or another run started, and on an event a workflow emitted: how many reactions are above it |
trigger | On a run a trigger started: its kind, event, cron or every, and its reference in the document |
trace_id, span_id | The trace and span of the append that recorded the event, one pair for every event of one append; with telemetry on, the trace the server exported |
What data shows
data keeps each event within 4 KiB:
- Large values are shown in full when each is at most 2 KiB as JSON. They are an input, an output, a record, an answer, a call's arguments and result, and a published event's data. A larger one is shown as its size in bytes, under its name plus
_bytes, such asoutput_bytes. - When the event's data would still be larger than 4 KiB, its largest field is shown as its size first.
- A definition's document and schemas appear as their sizes. Its description shows its first 300 characters, and its warnings as a count.
- A rejection shows its reason, its detail shortened to fit, and for invalid input the number of issues and the first five.
- A rejection that kept a record shows the record as a success does. A run of a reasoning function keeps one when it is rejected after its model answered.
The full values are read elsewhere: get_run returns a run's output and record, get_definition a definition's document and schemas, and get_event one event with every field in full.
Some events show more in data:
workflow_input_appliedshows the kind and key of the input and how many steps it moved. It lists the first five, each with its task, run and outcome. For a function's rejection that has a kind and abecause, it shows both, and its summary says them in words. It also shows the kinds of what the run did next. It never shows the run's data.- A step event shows the step's
nameand itsreferencein the document. It also showsrun, which counts each time the step starts, andtimes, which counts how often it moved that way in that run. step_waitingadds what it waits for,call,timerorevent. For a call it adds therun_idof the run it started, whoserun_startednames thatstep_waitingas its cause.step_failedadds how it failed, and its error's type and title.event_publishedshows the published event'sevent_id,event_type,source,subject,timeanddata, and which attributes the runtimefilledin.reaction_refusedshows how many times in theminutea trigger did not start its workflow, ascount, and the lastreason.
Some events say more in their metadata:
- For an event a workflow emitted,
metadatanames the run inrun_id, the workflow and its version indefinition, and its depth in a chain of triggers indepth. - A
reaction_refusedevent'smetadatanames the workflow and the version whose trigger refused, indefinition. - A run that a trigger started shows
playbook:and the playbook's id asby. Every event of it shows thetriggerthat started it in itsmetadata, with itskindandreference. Itsrun_startedsays in words which kind it was, such as "was started by its every schedule".
How events link
Each event's causation_id links the events of a run into a graph.
- A run you start has no cause.
- A workflow run's start is followed directly by its inputs and their steps. Its first input is caused by its start.
- A step's event has the id of the input that moved it, an underscore and its number among the steps that input moved, such as
<input id>_2. - A step's event is caused by what came before it in that input:
- the input, for the first step;
- the step before it in its list;
- the
switchthat chose it; - the
forkit is a branch of; - the failed attempt a retry repeats.
- A step that continues from an earlier input is caused by the input that moved it on. That input (a call's result, a delivered event or a fired timer) is caused by the input whose step waited for it.
- A run that a step's call started is caused by the input that started the call, and the run's end by the input that ended it.
- A run that an event trigger started names the event's record as its cause.
- A run that a schedule started names as its cause the record that saved its schedule in its current form. That record comes before the record of the run's
definition_versionwhen a later version left the schedule unchanged.
Reading one event
get_event reads one event in full by its id, as get_run_history and list_playbook_events show it. It has the same type, summary and metadata. Its data has every field in full. A call's arguments and result are read from what the playbook kept, and answer is the document read from the result. Use it to open a tool's result, or an input or output too large for a page, from the history.
- A step event's id goes in the path unchanged, such as
GET /events/0199a3c4-7d2e-7c1a-9b3f-2f1e0d9c8b7a_2. - Its number counts from 1 without a leading zero, so
_0and_02are refused withinvalid_input. - An id the playbook does not have, whether of another playbook or of a step its input did not move, returns
not_found.
Over MCP, a field that may be large is shown in full only up to 64 KiB, as the MCP reference says.
Paging
Every page carries has_more and next_cursor, on the page and not on its events. Pass next_cursor as cursor to read the next page, until next_cursor is null.
limitis 1 to 100, and 20 when left out.limitcounts the events a page returns, step events included. A page can end between the step events of one input; itsnext_cursorcontinues from the next one.- Cursors are opaque. A cursor this playbook did not give returns
invalid_inputat/cursor.
A page can contain fewer items than limit, or none, while has_more is true:
- records with no event type are left out;
- a page stops after loading 4 MiB of stored data;
- with
status,typeorname, a page stops after looking at 1,000 runs or records.
Analytics
| Operation | Method and route | Permission | Input |
|---|---|---|---|
get_playbook_analytics | GET /analytics | playbook:read | Optional days, or from and to; optional type and name |
The route is relative to /v1/orgs/{org}/playbooks/{playbook}. get_playbook_analytics returns what the playbook's runs did over a window of days, in UTC.
| Parameter | Rule |
|---|---|
days | 7, 14 or 30: the last days, ending today. 7 when nothing is given |
from, to | The first and the last day, as YYYY-MM-DD, both included: at most 366 days, with to not before from and not after today |
type, name | Keep the runs of one definition, as they do for list_runs |
- A day must exist in the calendar, so
2026-02-30is refused rather than read as 2 March. - These are refused with
invalid_input, pointing at the parameter:dayswithfromorto,fromwithoutto, any otherdays, and any other parameter.
GET /v1/orgs/acme/playbooks/sales/analytics?days=7&type=reasoning
Authorization: Bearer <key>{
"days": 7,
"runs": { "total": 12, "succeeded": 9, "failed": 1, "rejected": 2 },
"tokens": { "input": 18400, "output": 2950, "cached": 12000 },
"duration_ms": { "p50": 1840, "p95": 6210 },
"by_day": [
{
"day": "2026-09-30",
"runs": { "total": 0, "succeeded": 0, "failed": 0, "rejected": 0 },
"tokens": { "input": 0, "output": 0, "cached": 0 },
"duration_ms": null
}
],
"by_function": [
{
"type": "reasoning",
"name": "triage",
"runs": 12
}
]
}The example shortens by_day. It holds every day of the window, oldest first, including days with no runs.
| Field | Contents |
|---|---|
days | How many days the window covers |
runs | The runs that ended, by how they ended, and their total |
tokens | The input and output tokens the models of reasoning functions used, a rejected run's included when its model answered before the rejection; cached is the part of input read from the provider's cache; 0 where nothing was recorded |
duration_ms | p50 (the median) and p95 (the 95th percentile) of how long the runs that succeeded or failed took, in milliseconds from their latest start to their end; null when no run of the period has a duration |
by_day | The same runs, tokens and duration_ms for each day |
by_function | Each definition by type and name, with runs, how many of its runs ended; the most runs first, then by type and name |
How runs are counted:
- A run counts on the day it first started, also when it was started again later.
- A run counts once it has ended:
succeeded,failedorrejected. A run still going counts nowhere. - A run started again under its
run_idcounts once. Its tokens add up over every attempt that ended, while its duration is that of its last attempt. - Rejected runs count in
runsand intokens, never induration_ms. - Workflow runs count in
runsand induration_ms, from when the run started to when the workflow ended. - A percentile is the nearest rank: the duration at place ⌈p × n⌉ of the n durations in order.
The result comes from a table the runtime updates as each run is recorded, so it is as current as the runs themselves. A playbook that does not exist returns not_found; a retired playbook works like any other.
Tool servers
Five operations find and call the tools of the tool servers of a playbook: those the operator set up, and the connections people added to its org, MCP servers and HTTP APIs described by an OpenAPI document alike. A reasoning function names their tools in tools as server/tool or server/*, a connection by its name. These routes are relative to /v1/orgs/{org}:
| Operation | Method and route | Permission | Input |
|---|---|---|---|
search | GET /tool-servers/search | org:read or playbook:read | Optional playbook, terms, server and only |
search | GET /playbooks/{playbook}/tool-servers/search | playbook:read | Playbook id in path; optional terms, server and only |
introspect | GET /playbooks/{playbook}/tool-servers/{server}/tools/{tool} | playbook:read | Server and tool in the path |
validate | POST /playbooks/{playbook}/tool-servers/{server}/tools/{tool}/validate | playbook:read | Server and tool in the path; arguments in the body |
dry_run | POST /playbooks/{playbook}/tool-servers/{server}/tools/{tool}/dry-run | playbook:read | Server and tool in the path; optional arguments |
execute | POST /playbooks/{playbook}/tool-servers/{server}/tools/{tool}/execute | playbook:write | Server and tool in the path; optional arguments |
executeneedsplaybook:writefor every tool, since a call acts as the playbook, under the credential of the server's entry or of the connection.validateanddry_runare routed byPOSTbecause theirargumentsare an object a query string cannot carry. They are still reads: they change nothing, call no tool and need onlyplaybook:read. A query string beside their body returnsbad_request.- The four reads ask each server they need for its tool list when you call them, as a run does, with the same time limits to connect and to list. They record nothing. An HTTP API is not asked, since its OpenAPI document is the list.
- On a playbook route, a playbook that does not exist returns
not_found. A retired playbook serves the reads like any other. It refusesexecutewithconflictof the kindretired, as it refuses every command.
Searching the tools
search returns the servers of the playbook, those set up in the configuration and the connections of its org that serve it, and the tools of each that its entry allows.
- Without
terms, it lists every tool when the servers offer at most 100 in total, in the order of the servers and of each server's own list. When they offer more, it returns only the servers, each with its count of tools; search them withterms. termsis up to 8 words of up to 64 characters each, separated by spaces. Tools with a score come best first, those with the same score by name and then by server, at most 100.only: readkeeps the tools their server marks read-only, andonly: writethe others.serverkeeps the server of that name and asks no other. A name no server of the playbook has returnsinvalid_inputat/server.
Each word of terms, and each part of it split at _, ., - and case changes, scores in any letter case:
| Where the word appears | Score |
|---|---|
| A word of the tool's name, split the same way | 3 |
| A word of the name of one of the tool's arguments | 2 |
| A word of the tool's description | 1 |
GET /v1/orgs/acme/playbooks/sales/tool-servers/search?terms=invoice
Authorization: Bearer <key>{
"servers": [
{ "name": "billing", "type": "openapi", "record_content": true, "tool_count": 5 },
{
"name": "notes",
"type": "stdio",
"record_content": true,
"unavailable": "The tool server notes could not be used: The tool server could not be reached",
"because": "unreachable"
}
],
"tools": [
{ "server": "billing", "tool": "getInvoice", "summary": "Reads one invoice.", "read_only": true },
{
"server": "billing",
"tool": "listCustomers",
"summary": "Lists the customers, with the invoice each owes.",
"read_only": true
}
],
"scopes": ["billing", "notes"]
}| Field | Contents |
|---|---|
servers | The servers searched, sorted by name |
tools | The tools found |
scopes | The names of the servers searched |
Each server has these fields:
| Field | Contents |
|---|---|
name | The name a function writes before the slash |
connection | For a connection a person added, its id; absent for a server of the configuration. A connection whose name the configuration now sets up is listed beside that server, unavailable |
type | mcp for a remote MCP server, stdio for an MCP server the runtime starts as a process, or openapi for an HTTP API described by an OpenAPI document |
record_content | Whether the content of its calls is kept |
tool_count | How many tools of the server the operator allows, of those only keeps |
unavailable | Instead of tool_count: why the server could not be asked for its tools, in words cut to 1 KiB. The other servers are still searched |
because | With unavailable: key_refused when the server did not accept the key the runtime gives it, which only the operator can fix; failing, rate_limited or unreachable when asking again later may work; for a connection, connection_pending, connection_broken or connection_not_allowed, as below |
playbooks | On the org route only: the playbooks of the org whose functions may use it, or ["*"] for every playbook of the org |
Each tool has these fields:
| Field | Contents |
|---|---|
server | The server's name |
tool | The tool's name |
summary | The first sentence of its description, cut at 300 characters |
read_only | Whether its server marks it read-only |
What the result never contains:
- A server set up for another org, or for other playbooks of the org, is neither searched nor asked.
- The runtime adds no header, environment value, address, command or credential of a server to the result.
- What a server returns, such as its tools' names, descriptions and schemas, is passed on with two kinds of value scrubbed out. They are the values of the operator's
${...}references and the tokens minted for a server. A referenced value shorter than 8 characters is not scrubbed.
On the org route, search covers the org's playbooks. So you can see what the operator set up before a playbook exists, or without naming one:
- Without
playbook, it searches every server set up for the org, each withplaybooks. - With
playbook, it returns only the servers that serve that playbook, as the playbook's own route does, withplaybooksincluded. - A key limited to some playbooks must pass
playbook, and sees inplaybooksonly the playbooks it may access. Withoutplaybookit is refused withforbidden, whose detail says to name one of its playbooks inplaybook. - A
playbookthe org does not have returnsnot_found, as the playbook's own route does, and no server is asked. - A
serverthat no server of the org, or of the playbook given, has returnsinvalid_inputat/server.
Reading one tool
introspect returns one tool in full, as its server lists it:
- its
description, cut to 4 KiB; - its
input_schema, the JSON Schema of the arguments it takes; - its
annotations, the hints its server gives it (readOnlyHint,destructiveHint,idempotentHintandopenWorldHint), left out when the server gives none.
A tool of an HTTP API takes one object: each path and query parameter by its name, and body for a JSON body. The schemas from the document's components that it refers to are placed under $defs.
GET /v1/orgs/acme/playbooks/sales/tool-servers/billing/tools/getInvoice
Authorization: Bearer <key>{
"server": "billing",
"tool": "getInvoice",
"description": "Reads one invoice.",
"input_schema": {
"type": "object",
"properties": { "id": { "type": "string" } },
"required": ["id"],
"additionalProperties": false
},
"annotations": { "readOnlyHint": true, "idempotentHint": true, "openWorldHint": true }
}validate checks arguments, at most 16 KiB as JSON, against the tool's input schema. It returns valid (true or false) and issues. Each issue has the JSON Pointer of the argument at fault as path, and what is wrong there as message. It lists at most 20 issues, then one saying how many more there are. For a schema the runtime's checker cannot handle, such as one with a regular expression, it returns valid: null with one issue saying why; the arguments may still be right.
POST /v1/orgs/acme/playbooks/sales/tool-servers/billing/tools/getInvoice/validate
Authorization: Bearer <key>
Content-Type: application/json
{ "arguments": { "id": 42 } }{ "valid": false, "issues": [{ "path": "/id", "message": "Expected string" }] }dry_run reports what execute would check for the tool and its arguments, without calling the tool:
| Field | Contents |
|---|---|
would_run | Whether execute would call the tool now: the caller may, the playbook is not retired, the operator allows the tool, and its server responded |
needs | playbook:write, the permission execute needs whatever the tool does |
caller_may | Whether the caller has that permission |
playbook_retired | Whether the playbook is retired, which execute refuses, since a retired playbook can no longer change |
allowed | Whether the operator allows the tool on its server's entry, for runs and for execute alike |
read_only | Whether the server marks the tool read-only; null when the tool's listing was not read, for a tool the operator does not allow or a server that could not be asked |
arguments | The arguments checked as validate checks them, valid: null with one issue saying why when the tool's input schema was not read; null when none were given |
server | reachable (yes or no) and, with no, the because field that says why the server could not be asked |
dry_run returns would_run: true even for arguments the tool's schema does not accept. execute does not check arguments, and the tool returns its own error for them. The arguments field reports the problem beforehand.
When a server or a tool cannot be used, the reads respond as below. Each refusal is unavailable, and no tool is called:
| Case | introspect and validate | dry_run |
|---|---|---|
| A server not set up for the playbook | Kind tool_not_offered, because: "tool_server_not_configured", before any server is asked | The same |
| A tool the operator does not allow | Kind tool_not_offered, because: "tool_not_allowed", before any server is asked | Returns allowed: false |
| A tool the server does not list | Kind tool_not_offered, because: "tool_not_listed" | The same |
| A connection that is not ready | Kind tool_not_offered, with its because, as below | The same, or server.reachable: "no" with because: "connection_not_allowed" for an address its document names |
| A server that cannot be asked | Kind tool_server_failed, with its because, as execute returns it | Returns server.reachable: "no", with its because |
Calling a tool by hand
execute calls one tool of one tool server with the arguments given, as a run of a reasoning function would call it. It returns what that run's model would see, and the document an interaction function's call reads. Use it to see what a tool returns before a function names it, or to find an id a prompt needs. Use it, too, to do what the person asked of the tool without first creating a function.
arguments is the object the tool's input_schema takes, {} when left out. It may be at most 16 KiB as JSON; larger arguments return invalid_input at /arguments before anything is called.
POST /v1/orgs/acme/playbooks/sales/tool-servers/billing/tools/getInvoice/execute
Authorization: Bearer <key>
Content-Type: application/json
{ "arguments": { "id": "inv-2041" } }{
"call_id": "0199b7e2-4c1d-7a3e-8f5b-6d2c1e0f9a8b",
"server": "billing",
"tool": "getInvoice",
"outcome": "result",
"text": "{\"id\":\"inv-2041\",\"total\":1200}",
"answer": { "id": "inv-2041", "total": 1200 },
"result_bytes": 98,
"duration_ms": 312,
"called_at": "2026-10-08T09:00:00.000Z"
}| Field | Contents |
|---|---|
call_id | The id of the call, a UUID, under which its two events are recorded |
outcome | How the call ended, from the table below |
text | What a reasoning function's model would see, scrubbed of the server's secrets and cut at 64 KiB with a note. Text content is text; structured content is JSON only when there is no text; other content is a placeholder. For a failure, the words the model gets |
answer | The document a call's read points into, scrubbed of the server's secrets: the structured content, else the first text block parsed as JSON, else that text. Absent when the tool returned neither, or when it is larger than 64 KiB as JSON |
result_bytes | The size of the whole result as the playbook keeps it, scrubbed of the server's secrets, before the cut; null when there is none |
duration_ms | How long the call took |
server_request_id | The server's own id of the request, when the operator's request_id setting says where the server carries one |
called_at | When the call was made, in ISO 8601 UTC |
outcome | Means |
|---|---|
result | The tool returned a result |
tool_error | The tool returned an error or refused the arguments; a run's model may recover from this |
server_failure | The server failed on the call |
timed_out | The server did not respond within 30 seconds |
too_large | The tool's result was larger than one call may return |
The response is 200 whenever the call was made, whatever the tool returned. Nothing is called when:
- the operator does not allow the tool, the server is not set up for the playbook, or the server does not list the tool:
unavailablewith kindtool_not_offeredandbecauseset totool_not_allowed,tool_server_not_configuredortool_not_listed, or for a connection that is not readyconnection_pending,connection_brokenorconnection_not_allowed; - the server cannot be used:
unavailablewith kindtool_server_failed.
A call is live: a tool its server does not mark read-only may change something. That is why execute is marked destructive over MCP while a tool server is configured or people may add connections.
Each call is recorded in the playbook's history: as tool_call_started before it is sent, with connection_id beside server for a connection, and as tool_call_answered or tool_call_failed after. These are the same facts a run's tool calls record, with the caller who made the call as the one who acted.
- The call is recorded on a stream of its own, with no run.
list_runsandget_run_historynever show it, whilelist_playbook_eventsdoes. - The arguments and the result are kept unless the operator turns
record_contentoff for the server. - A caller that disconnects during the call leaves a start without an answer.
- When no run holds a connection to the server, each call opens one and closes it once the call ends.
Connections
A connection is a tool server that a person adds to the org through their agent, which every playbook of the org may use, or the playbooks its entry names. The agent adds it without its credential; the person approves it, and gives its credential where it takes one, on Studio's page, which its link opens. Let people add connections sets out the flow, the kinds of credential and the rules the operator sets. These routes are relative to /v1/orgs/{org}:
| Operation | Method and route | Permission | Input |
|---|---|---|---|
add_connection | POST /connections | org:write | name, kind, entry, credential |
list_connections | GET /connections | org:read | Optional include_removed |
get_connection | GET /connections/{id} | org:read | Connection id in path |
remove_connection | DELETE /connections/{id} | org:write | Connection id in path |
read_link | GET /connections/{id}/link/{token} | any, with the link's token | Connection id and token in path |
approve | POST /connections/{id}/approve | any, with the link's token and the adder's session | token |
give_credential | POST /connections/{id}/credential | any, with the link's token and the adder's session | token, and value for a secret, or client_secret, or private_key with algorithm, for client_credentials |
Every operation answers the connection: id, a UUIDv7; name; kind, mcp or openapi; entry; credential, how it is credentialled, never its value; status, pending, connected, broken or removed; added_by; added_at; connected_at; connected_by, the id of the caller who connected it, by approving it or giving its credential, or null; credential_ends_with, the last four characters of a secret or client secret of twelve characters or more, which the secret store keeps beside the value, or null; and broken, why it is broken, or null. add_connection answers 201 with the connection and connect, { url, expires_at }: url is Studio's page, https://studio.on.auto/connect?server=<the runtime's origin>&org=<org>&connection=<id>&token=<token>, whose origin is PUBLIC_URL, or the server's own address in local mode, and the link expires ten minutes on. list_connections answers them newest first in connections, the removed ones only with include_removed.
entryholds the server without its credential:urlformcp;documentandbase_urlforopenapi; andheaders,query_params,allowed,playbooks,record_contentandrequest_id, as an entry of the configuration file has them.urlanddocumentare each anhttpsURL, or anhttpURL on a loopback address.credentialis one object,kindand the fields that kind takes, each optional in the schema and refused asinvalid_inputat the field where its kind lacks it or does not take it:nonetakes no other field;secrettakesnameandin,header,queryorsecurity, sonameis the header, the query parameter or the security scheme of the document the value is sent as, and only anopenapiconnection takesqueryorsecurity;referencetakesvariable, a variable of the runtime such asCRM_API_KEYthat the operator lists inconnections.variables, whose value is sent where itsnameandinsay, as a secret's; andclient_credentialstakesissuer, anhttpsURL or anhttpURL on a loopback address,client_idand an optionalscope. The same object is what every operation answers and what the org's record keeps.get_connection, which Studio reads, and the three routes of the link are served over HTTP alone, and no MCP endpoint serves them. The three routes of the link are Studio's page's alone. Each takes the token, which the runtime keeps only as its SHA-256, and matches it to the open link of the connection: one given less than ten minutes ago that nothing has used up.approveconnects a connection whose credential isnoneorreferenceand uses its link up.give_credentialkeeps the value sealed in the runtime's secret store, connects the connection and uses its link up: giving the credential is its approval.- A connection is added by a person: on a runtime that signs people in,
add_connectionrefuses a key, and a runtime of API keys alone, which signs nobody in, refuses every connection, since no person could approve it. - On a runtime that signs people in,
approveandgive_credentialneed the session of the person who added the connection. In local mode the token alone binds. remove_connectionremoves a connection for good, and the runtime forgets its credential. Removing a removed connection succeeds and records nothing. Its name may then be given to a new connection.
| Case | Response |
|---|---|
An entry that holds a value that looks like a credential, a reference such as ${NAME}, or a field another entry would refuse | invalid_input |
| A kind of credential there is not, or a field its kind lacks or does not take | invalid_input |
| A connection the operator's rules do not take: no one may add one, its host, its kind of credential or the variable its reference names | forbidden, with the sentence of the rule |
A name of an entry of tool_servers, or of a connection of the org that is not removed | conflict |
| An org that holds 50 connections that are not removed, the most it may | conflict, kind connections_full |
| A token that is unknown, used up or expired, or an id the org does not have | not_found |
add_connection by a key where people sign in, or by anyone on a runtime of API keys alone | forbidden |
approve or give_credential with the session of another person, where people sign in | forbidden |
approve of a connection whose credential is to be given, or give_credential of one that takes none | conflict |
A connection that is not ready
A connected connection is a tool server of its org: search lists it with connection, its id, a function names it by its name as name/tool, and introspect, validate, dry_run and execute reach it, as Tool servers describes. The runtime checks the operator's rules and the entry, as add_connection checks them, and reads its credential when it is first used after the runtime starts. A connection that is not ready is refused as unavailable of the kind tool_not_offered, and search lists it unavailable with the same because and words:
because | The connection |
|---|---|
connection_pending | Nobody has approved it yet; the person who added it opens its link, or removes it and adds it again |
connection_broken | Its credential cannot be read, and the runtime recorded it broken, with why in broken; the person removes it and adds it again |
connection_not_allowed | The runtime does not use it, in the words of why: a rule of the operator no longer takes it, in the sentence add_connection would give; connections.people_may_add is false; its entry is no longer one the runtime takes; the variable its reference names is not set; the configuration now sets up a server of its name; or a request it would make goes to a host the rules do not allow |
Responses and errors
A successful operation responds with JSON and Cache-Control: no-store. The create operations, create_playbook and create_definition, return 201; other successful operations return 200.
An error is an RFC 9457 problem document, with Content-Type: application/problem+json. Inspect its reason and detail:
invalid_inputincludes anerrorslist of JSON pointers.- A rejection that has a
kindcarries it. - An
unavailableorconflictrejection that has abecausecarries it. - A
conflictmay have the kindoversized, for a workflow whose output is larger than a run can record.
A refusal of a function's input says each issue in its detail, one sentence each, in the words of the input schema. A sentence names the field, then its description and examples, then the rule it broke. Each entry of errors holds one sentence and the field's pointer, so a reader can show it at its field:
{
"type": "https://on.auto/problems/invalid_input",
"title": "Invalid input",
"status": 422,
"detail": "month, The month to report on, such as \"April 2026\" or \"2026-04\": expected at least 6 characters.",
"reason": "invalid_input",
"errors": [
{
"detail": "month, The month to report on, such as \"April 2026\" or \"2026-04\": expected at least 6 characters.",
"pointer": "/input/month"
}
]
}A nested field is named by its path, such as address, then city, and an item of a list by its place, such as lines, then item 2. A missing field is named at the field that holds it, such as The input: expected the field month. A workflow whose step refused its input is rejected with that step's errors, at the pointers the step saw.
| Status | Common reason |
|---|---|
| 400 | bad_request |
| 401 | Missing or invalid credentials |
| 403 | forbidden or origin_not_allowed |
| 404 | not_found |
| 405 | Unsupported method; see the Allow header |
| 409 | conflict or cancelled |
| 410 | unanswered |
| 411 | length_required |
| 413 | content_too_large |
| 415 | unsupported_media_type |
| 422 | invalid_input |
| 500 | internal |
| 503 | unavailable |
A problem document's type is a URI that names its kind of problem:
| Type | Status | Means |
|---|---|---|
https://on.auto/problems/bad_request | 400 | The request is malformed, such as a field given twice or a body that is not a JSON object |
https://on.auto/problems/unauthenticated | 401 | Credentials are missing or invalid |
https://on.auto/problems/forbidden | 403 | The credentials may not do that |
https://on.auto/problems/origin_not_allowed | 403 | A browser sent the request from an origin the server does not allow |
https://on.auto/problems/not_found | 404 | Something the request names does not exist |
https://on.auto/problems/method_not_allowed | 405 | The path does not take that method |
https://on.auto/problems/conflict | 409 | The request clashes with what is there |
https://on.auto/problems/tools_called | 409 | A run that may have called tools is not run again under its id; its reason is conflict. When every tool the run called only reads, it has because: "only_read", and its words say nothing was changed |
https://on.auto/problems/effect_unknown | 409 | A run called a tool its server does not mark read-only, and could not finish. Whether the tool changed something is not known. Its reason is conflict; a person, or a workflow rule that names it, decides what happens next |
https://on.auto/problems/cancelled | 409 | The run was cancelled, with the kind of its cancellation; its reason is cancelled |
https://on.auto/problems/unanswered | 410 | Nobody answered the request a run waited on, of an interaction function or a prediction function's build, with the kind expired or undelivered |
https://on.auto/problems/length_required | 411 | A body a route streams, a dataset or a candidate, was sent without its Content-Length |
https://on.auto/problems/content_too_large | 413 | The body is larger than the operation accepts: 1,048,576 bytes of JSON, or 64 MiB for a dataset or a candidate. Their Content-Length is refused before the body is read |
https://on.auto/problems/unsupported_media_type | 415 | The body is not sent as the media type the operation accepts, application/json in UTF-8 unless the route says otherwise, or is sent compressed |
https://on.auto/problems/invalid_input | 422 | The input does not fit, with the errors that point at it |
https://on.auto/problems/client_closed_request | 499 | The client disconnected before the response |
https://on.auto/problems/internal | 500 | Something went wrong inside the server |
https://on.auto/problems/unavailable | 503 | Something the server relies on cannot serve now |
https://on.auto/problems/tools_unfinished | 503 | A run called only tools their servers mark read-only, and could not finish; its reason is unavailable, and running it again under a new id is safe |
https://on.auto/problems/rebuilding | 503 | The view of a recall function is still being built; its reason is unavailable; try again later |
When a tool-calling run cannot finish explains when a run ends as tools_unfinished or effect_unknown. Retrying under a run id says when a request returns tools_called.
Inside a workflow, two rejections of a run the workflow called differ from their HTTP form:
invalid_inputis status 422 over HTTP, but a workflow raises it as avalidationerror of status 400. Acatchfor it matches status 400 or thevalidationtype, never 422.- A cancellation of a called run that the workflow does not catch rejects the workflow as
invalid_input, not ascancelled, since the workflow itself was not cancelled.
When the function's run does not succeed has the full table of what a workflow catch sees.
A 500 response contains an incident reference: its instance and the x-request-id response header identify the server log entry. Include that reference when reporting a problem, without sharing credentials or confidential input. A malformed HTTP request can get a bare status before the API handles it.
When to retry
Retry-After comes only with an unavailable response that a retry of the same request may resolve. It never comes:
- with the kinds
tools_unfinished,tool_not_offeredormodel_not_offered; - with
becauseset tobuilds_of_the_day,stalled,not_finiteornot_in_order, which no wait resolves; - with
cancelled.
Models and tools a function may not use
| Case | Response |
|---|---|
| A function names a model outside the deployment's allow list | unavailable with kind model_not_offered and because: "model_not_allowed", before the provider is called |
| A function names a model whose provider is missing | unavailable with kind model_not_offered and because: "provider_not_configured" |
| A function names a tool the playbook's servers do not offer | unavailable with kind tool_not_offered |
introspect, validate, dry_run or execute names a tool the playbook is not offered | unavailable with kind tool_not_offered |
Use list_models to see the model references the server offers, and search to see the servers and tools a function may name.