Skip to content

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 GET and DELETE requests, 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:

PermissionLets the caller
org:readRead the org's playbooks and connections, list models and search the org's tool servers
org:writeCreate, update and retire playbooks, and add and remove connections
playbook:readRead definitions, runs, history, events, requests and analytics, and search and read tools
playbook:writeCreate, 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 routeReturns
GET /v1/openapi.jsonThe OpenAPI 3.1.1 document of this API
GET /v1/guides/{guide}One guide, as text/markdown
GET /.well-known/api-catalogThe API catalog of this server, as application/linkset+json
GET /healthWhether 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 operationId is the operation's name, with search_in_org for the org's search. 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_analytics and validate.
  • Its input is in the path, and in the query string of a GET. A command, or a query routed by POST such as validate and dry_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 $id under https://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_URL when 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, HEAD included, carry Link: <…/.well-known/api-catalog>; rel="api-catalog". GET /health carries 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 ETag of its bytes and Cache-Control: public, max-age=300.
  • Each responds with 304 and no body when the request's If-None-Match names its tag, a list that includes it, or *.
  • HEAD returns their headers without the body.
  • /health is never cached: it responds with Cache-Control: no-store.

Playbooks

These routes are relative to /v1/orgs/{org}:

OperationMethod and routePermissionInput
create_playbookPOST /playbooksorg:writeplaybook, name, optional description
list_playbooksGET /playbooksorg:read or playbook:readOptional include_retired
get_playbookGET /playbooks/{playbook}org:readPlaybook id in path
update_playbookPUT /playbooks/{playbook}org:writename and description
retire_playbookPOST /playbooks/{playbook}/retireorg:writePlaybook id in path
FieldRule
playbookThe id: 3 to 48 lowercase letters, digits and hyphens, beginning with a letter
name1 to 100 characters, stored without the whitespace around it; it cannot be all whitespace
descriptionAt 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_playbooks lists the playbooks the caller may access. It leaves out retired ones unless include_retired asks for them. It also accepts a key with only playbook: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_playbook requires both name and description, 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_playbook reads it, include_retired lists it, and its definitions, runs and history remain readable. Commands inside it return conflict with the kind retired. Retiring it again succeeds and changes nothing.
CaseResponse
create_playbook with an id the org has or hadconflict
update_playbook of a retired playbookconflict
get_playbook, update_playbook or retire_playbook of an id the org does not havenot_found
A change that meets another change to the org's playbooks at the same momentconflict; send it again

Models

OperationMethod and routePermissionInput
list_modelsGET /v1/orgs/{org}/modelsorg:readOptional 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}:

OperationMethod and routePermissionInput
create_definitionPOST /definitions/reasoningplaybook:writename, source
list_definitionsGET /definitions/reasoningplaybook:readOptional include_retired
get_definitionGET /definitions/reasoning/{name}playbook:readName in path
update_definitionPUT /definitions/reasoning/{name}playbook:writesource
retire_definitionPOST /definitions/reasoning/{name}/retireplaybook:writeName in path
run_definitionPOST /definitions/reasoning/{name}/runplaybook:writeOptional input, optional UUID run_id
get_runGET /runs/{run_id}playbook:readRun 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_schema and output_schema where its document declares them. A reasoning function, a computation function or a workflow whose output.shape names an output shape has the shape's schema as its output_schema, and the shape's name as details.shape, such as "details": { "shape": "series" }; details is left out otherwise. A prediction function has the series shape so, since its questions answer a series. list_definitions shows 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_definition calls 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 their subject) or on its changes (by their source).
  • 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.
CaseResponse
A to or an argument the function cannot renderconflict with the kind unworkable
A server or a tool the playbook may not useunavailable with the kind tool_not_offered
The playbook has as many open requests as the runtime allowsunavailable with the kind requests_full

The requests of a playbook have two routes of their own, relative to /v1/orgs/{org}/playbooks/{playbook}:

OperationMethod and routePermissionInput
list_interactionsGET /interactionsplaybook:readOptional to, type, function, limit and cursor
answer_interactionPOST /runs/{run_id}/answerplaybook:writeRun 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.

  • to keeps only the requests to one party.
  • type keeps only those from one type of function.
  • function keeps only those from functions of one name. A name is unique within its type, so function with type names one function.

Each request has these fields:

FieldContents
run_idThe run that asked, which answer_interaction takes
typeThe type of the function that asked
functionThe name of the function that asked
versionThe version of the function that asked
toThe party the request goes to
deliveryThe server and tool the request is sent through, or null for the inbox
messageThe message of the request
takes_answertrue for a question, false for a notification
answer_schemaThe JSON Schema an answer must match, as the request recorded it when it was asked; null for a notification
requested_atWhen the request was made, in ISO 8601 UTC
expires_atWhen the request expires unanswered, in ISO 8601 UTC
attemptsThe delivery attempts made so far
conversationThe conversation the playbook reads replies in, or null
answererThe party whose reply counts, or null
reply_refusalsHow many replies from the answerer the playbook refused
standingin_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:

  • answer must match the request's answer schema, and may be at most 64 KiB as JSON.
  • claimed_for names 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:

FieldContents
run_idThe run that asked
answer_idThe id of the fact
answerThe answer
claimed_forWhom the caller says it is answering for, when given
answered_byThe caller's id; for a run answered by a reply, the playbook
replyFor a run answered by a reply, that reply's identity
answered_atWhen 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 as invalid_input.
CaseResponse
An answer that does not matchinvalid_input with pointers under /answer; the request stays open
The same answer againThe 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 notificationconflict
A run the playbook does not havenot_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.

CaseResponse
A ready checker does not finish the check within 2 seconds of the server's wall-clock timeinvalid_input; nothing is saved
A save finds no checker readyunavailable (503); the same call may be tried again
The program cannot produce its output for the inputconflict 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_definition adds the view's standing. get_run adds to the run's record the checkpoint the run read at, which says how far into the playbook's history the view had read. See How the view is kept.
CaseResponse
The view of the latest version is still being builtunavailable with the kind rebuilding and a Retry-After
The view has stalledconflict with the kind stalled
The module's answer cannot produce an outputconflict 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 whose observed line holds the last periods of the dataset and whose predicted line holds the period asked, with its value, low and high; without horizon it holds every horizon the function names. The run's record holds the model's version and the coverage. The input and the output has every field, and A question an answer in full.
  • A build (the input { build: {} }) responds with 200 and status: started, then waits for its trainer, as A build explains.
  • get_definition adds 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}:

OperationMethod and routePermissionBodyReturns
upload_datasetPUT /datasets/{sha256}playbook:writetext/csv with Content-Length, at most 64 MiB{ dataset, bytes, rows, series, columns, period, ... }
read_build_briefGET /runs/{run_id}/build/briefplaybook:readNoneThe brief, text/markdown
read_build_rowsGET /runs/{run_id}/build/rowsplaybook:readNoneThe training rows, text/csv, streamed
upload_candidatePOST /runs/{run_id}/build/candidatesplaybook:writeAn ONNX model, application/octet-stream with Content-Length, at most 64 MiB{ candidate, bytes, opset }
activate_versionPOST /definitions/prediction/{name}/activateplaybook:write{ version }{ function, version, rule, activated_at }
  • A body sent without its Content-Length gets 411. A body whose Content-Length is larger than the route accepts gets 413, before the body is read.
  • The build itself is answered through answer_interaction, with { candidates }: the SHA-256 of each candidate, as upload_candidate returned it.

The three build routes work only on a build that is waiting for its answer:

CaseResponse
A run the playbook does not havenot_found
A run that is not a buildconflict with the kind not_a_build
A build that took its answer or endedconflict 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}:

OperationMethod and routePermissionInput
create_definitionPOST /definitions/workflowplaybook:writename, source
list_definitionsGET /definitions/workflowplaybook:readOptional include_retired
get_definitionGET /definitions/workflow/{name}playbook:readName in path
update_definitionPUT /definitions/workflow/{name}playbook:writesource
retire_definitionPOST /definitions/workflow/{name}/retireplaybook:writeName in path
run_definitionPOST /definitions/workflow/{name}/runplaybook:writeOptional input, optional UUID run_id
get_runGET /runs/{run_id}playbook:readRun id in path
cancel_runPOST /runs/{run_id}/cancelplaybook:writeOptional reason
send_run_eventPOST /runs/{run_id}/eventsplaybook:writeevent 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_type application/yaml.
  • Its description, input_schema and output_schema come from the document, the output_schema from its output.shape where it names one, with details.shape.
  • A workflow whose document has a schedule also has triggers, in get_definition and list_definitions. They are the triggers its schedule names, in the order it names them.
Trigger fieldContents
kindevent, cron or every
reference/schedule/on, /schedule/cron or /schedule/every
filtersFor an event trigger: its filters, each with its reference, type and attributes
expressionFor a cron trigger: its expression
millisecondsFor 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_definition returns 200 as soon as the run begins, with its run_id and status: started. A run that ended before its first wait returns how it ended.
  • Read the run with get_run until its status is succeeded, rejected or failed.
  • Read its steps with get_run_history, which contains one workflow_input_applied event for each input the run took (see Run history and playbook events).
  • While the runtime is stopping, run_definition returns unavailable; 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 eventLimit
The whole event256 KiB as JSON
type, id256 characters each
sourceA URI reference such as /ledger/eu, as for a published event; 1,024 characters
subject1,024 characters
dataNests 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.

CaseResponse
A type or source the runtime reserves for its own facts, as listed under Publishing eventsinvalid_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 spaceinvalid_input
The playbook has no running workflow with that run id, including one that has endednot_found
The run cannot take the event at that moment, such as while the runtime is stoppingunavailable; 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.

  1. The request is recorded on the run at once, whichever server takes it. cancel_run returns 200 with the run as it stands, still started.
  2. Within a moment the run stops its tasks and cancels each run it waits for.
  3. It ends rejected with the reason cancelled and the kind requested. Read it with get_run.

Asking again before the run has ended records nothing more.

CaseResponse
The playbook has no run with that idnot_found
The run has endedconflict
The run ends within the request that started itconflict

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:

KindThe run was cancelled because
requestedsomeone allowed to change the playbook asked, with cancel_run
deadlinethe workflow that waited for it ran out of time
overrunit ran as long as a workflow may run
parent_endedthe 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 record is 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.

WhatLimit
Input256 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 together1 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:

TypeLargest output
ReasoningWhat 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 call1 MiB less the run's record, measured exactly
Computation1,048,320 bytes: the function keeps 256 bytes for its record
Recall1,046,528 bytes: the function keeps 2,048 bytes for its record, which also says where the view had read to
Workflow1,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 idA request with the same id and input
SucceededReturns the recorded output; nothing runs again
Was rejected as invalid_inputReturns the same rejection
Was cancelledReturns its cancellation, cancelled (409)
Went unansweredReturns unanswered (410), as A request nobody answers says
Still waits: a workflow run, an interaction function's request, or a prediction function's buildReturns 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 interruptionRuns 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 conflict with the kind tools_called. When the run has ended and every tool it called is one its server marks read-only, the rejection adds because: "only_read", and its words say nothing was changed.
  • A run of such a function that is still started returns tools_called too, 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_offered or tool_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.

OperationMethod and routePermissionInput
publish_eventPOST /v1/orgs/{org}/playbooks/{playbook}/eventsplaybook:writeevent

The body contains event, a CloudEvents 1.0 event:

AttributeRequiredContentsLimit
sourceYesWhere the event comes from, a URI reference such as /ledger/eu or https://acme.example/ledger1,024 characters
typeYesWhat happened, such as com.acme.ledger.month-closed256 characters
idNoUnique among the events of its source; the runtime makes one when you leave it out256 characters
timeNoWhen 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
subjectNoWhat the event is about within its source1,024 characters
specversionNo1.0
datacontenttype, dataschemaNoThe media type of data, such as application/json, and the absolute URI of a schema it follows
dataNoAny JSON valueNests at most 510 levels
  • The event, with its id and time filled in, may be at most 240 KiB as JSON.
  • data nests at most 510 levels deep, so that a run can keep the event in a list. A deeper one returns invalid_input at /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, type and subject need a character that is not a space;
  • datacontenttype is a media type;
  • a time has a second of 60 only at the end of a day in UTC.
http
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:

json
{ "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, time and recorded_at.
  • A retry that leaves out time is the same event.
  • A retry that gives a time to 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:00 is the same time as 2026-10-01T08:59:00Z.
  • A leap second counts as the first second of the next day, so 2016-12-31T23:59:60Z is the same time as 2017-01-01T00:00:00Z.
  • A different event with the same source and id returns conflict.
  • 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:

ReservedNamesRefused at
TypesEvery type the playbook's events show and every fact the playbook's capabilities declare, listed below/event/type
SourcesThose beginning /runs/, /definitions/ or /callers//event/source
Attributescausationid, 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}:

OperationMethod and routePermissionInput
list_runsGET /runsplaybook:readOptional type, name, status, limit and cursor
get_run_historyGET /runs/{run_id}/historyplaybook:readRun id in path; optional order, limit and cursor
list_playbook_eventsGET /eventsplaybook:readOptional type, since, run_id, order, limit and cursor
get_eventGET /events/{event_id}playbook:readEvent 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_run returns, without output, record and the detail and issues of a rejection.
  • A rejection shows its reason, with kind and because when the function gave them.
  • status keeps only the runs whose status is started, succeeded, rejected or failed.
  • type and name keep 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, with read_only: true when the server marks the tool read-only, and connection_id beside server when the server is a connection a person added;
  • then tool_call_answered when the tool answered, with is_error when its result is an error of its own;
  • or tool_call_failed when it never answered, with because: arguments_refused, server_failure, timed_out, cancelled, or too_large when 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.

EventShows
interaction_requestedThe delivery, the party, the size of the message and the expiry
delivery_startedThe tool, the connection's connection_id where its server is one, and the size and digest of its arguments
delivery_succeededWhere the request landed and where its replies are read
delivery_failedbecause, including too_large for a result larger than one call may return, and retry_after_ms when the server asked for a delay
delivery_refusedbecause, too_large or unworkable
request_answeredThe answer the run took, claimed_for and, for a reply, its identity
reply_refusedEach 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_failed or step_skipped;
  • step_started for a step that runs others, such as a do or a fork, 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_read or reading_failed;
  • its tellings, telling_started and then telling_succeeded or telling_failed;
  • the inputs workflow runs took;
  • the events published to the playbook.
FilterKeeps
typeOne event type
sinceWhat the playbook recorded from that time on, in either order
run_idWhat 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_sha256 or result_sha256, is the key it is kept under. It is present only when the text was kept.
  • The history shows arguments and answer when each is at most 2 KiB, and otherwise their sizes. get_event reads 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:

FieldContents
idThe 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
typeWhat happened, such as run_started, delivery_failed or step_finished
summaryA sentence in plain language
dataThe fact itself, at most 4 KiB as JSON
metadataWhere 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:

FieldContents
streamThe stream the event is recorded in, such as playbook/acme/finance/runs/<run id>
positionThe event's number in its stream, from 1
global_positionThe store's sequence across every playbook, with gaps; it is not the order a page reads events in
correlation_idThe id of the run at the top of the chain the event belongs to, or null
causation_idThe id of the event that directly led to this one, or null when nothing recorded did, such as for a run you started
atWhen it happened, by its own clock, in ISO 8601 UTC
byWho 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_idThe run the event belongs to, when it belongs to one
definitionThe type, name and, when the event names one, version of the definition the run carries out or the event is about
called_byOn 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_depthOn a run a call started: how many calls are above it
depthOn a run a trigger or another run started, and on an event a workflow emitted: how many reactions are above it
triggerOn a run a trigger started: its kind, event, cron or every, and its reference in the document
trace_id, span_idThe 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 as output_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_applied shows 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 a because, 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 name and its reference in the document. It also shows run, which counts each time the step starts, and times, which counts how often it moved that way in that run.
  • step_waiting adds what it waits for, call, timer or event. For a call it adds the run_id of the run it started, whose run_started names that step_waiting as its cause.
  • step_failed adds how it failed, and its error's type and title.
  • event_published shows the published event's event_id, event_type, source, subject, time and data, and which attributes the runtime filled in.
  • reaction_refused shows how many times in the minute a trigger did not start its workflow, as count, and the last reason.

Some events say more in their metadata:

  • For an event a workflow emitted, metadata names the run in run_id, the workflow and its version in definition, and its depth in a chain of triggers in depth.
  • A reaction_refused event's metadata names the workflow and the version whose trigger refused, in definition.
  • A run that a trigger started shows playbook: and the playbook's id as by. Every event of it shows the trigger that started it in its metadata, with its kind and reference. Its run_started says in words which kind it was, such as "was started by its every schedule".

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 switch that chose it;
    • the fork it 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_version when 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 _0 and _02 are refused with invalid_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.

  • limit is 1 to 100, and 20 when left out.
  • limit counts the events a page returns, step events included. A page can end between the step events of one input; its next_cursor continues from the next one.
  • Cursors are opaque. A cursor this playbook did not give returns invalid_input at /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, type or name, a page stops after looking at 1,000 runs or records.

Analytics

OperationMethod and routePermissionInput
get_playbook_analyticsGET /analyticsplaybook:readOptional 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.

ParameterRule
days7, 14 or 30: the last days, ending today. 7 when nothing is given
from, toThe 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, nameKeep the runs of one definition, as they do for list_runs
  • A day must exist in the calendar, so 2026-02-30 is refused rather than read as 2 March.
  • These are refused with invalid_input, pointing at the parameter: days with from or to, from without to, any other days, and any other parameter.
http
GET /v1/orgs/acme/playbooks/sales/analytics?days=7&type=reasoning
Authorization: Bearer <key>
json
{
  "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.

FieldContents
daysHow many days the window covers
runsThe runs that ended, by how they ended, and their total
tokensThe 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_msp50 (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_dayThe same runs, tokens and duration_ms for each day
by_functionEach 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, failed or rejected. A run still going counts nowhere.
  • A run started again under its run_id counts once. Its tokens add up over every attempt that ended, while its duration is that of its last attempt.
  • Rejected runs count in runs and in tokens, never in duration_ms.
  • Workflow runs count in runs and in duration_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}:

OperationMethod and routePermissionInput
searchGET /tool-servers/searchorg:read or playbook:readOptional playbook, terms, server and only
searchGET /playbooks/{playbook}/tool-servers/searchplaybook:readPlaybook id in path; optional terms, server and only
introspectGET /playbooks/{playbook}/tool-servers/{server}/tools/{tool}playbook:readServer and tool in the path
validatePOST /playbooks/{playbook}/tool-servers/{server}/tools/{tool}/validateplaybook:readServer and tool in the path; arguments in the body
dry_runPOST /playbooks/{playbook}/tool-servers/{server}/tools/{tool}/dry-runplaybook:readServer and tool in the path; optional arguments
executePOST /playbooks/{playbook}/tool-servers/{server}/tools/{tool}/executeplaybook:writeServer and tool in the path; optional arguments
  • execute needs playbook:write for every tool, since a call acts as the playbook, under the credential of the server's entry or of the connection.
  • validate and dry_run are routed by POST because their arguments are an object a query string cannot carry. They are still reads: they change nothing, call no tool and need only playbook:read. A query string beside their body returns bad_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 refuses execute with conflict of the kind retired, 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 with terms.
  • terms is 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: read keeps the tools their server marks read-only, and only: write the others.
  • server keeps the server of that name and asks no other. A name no server of the playbook has returns invalid_input at /server.

Each word of terms, and each part of it split at _, ., - and case changes, scores in any letter case:

Where the word appearsScore
A word of the tool's name, split the same way3
A word of the name of one of the tool's arguments2
A word of the tool's description1
http
GET /v1/orgs/acme/playbooks/sales/tool-servers/search?terms=invoice
Authorization: Bearer <key>
json
{
  "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"]
}
FieldContents
serversThe servers searched, sorted by name
toolsThe tools found
scopesThe names of the servers searched

Each server has these fields:

FieldContents
nameThe name a function writes before the slash
connectionFor 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
typemcp 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_contentWhether the content of its calls is kept
tool_countHow many tools of the server the operator allows, of those only keeps
unavailableInstead 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
becauseWith 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
playbooksOn 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:

FieldContents
serverThe server's name
toolThe tool's name
summaryThe first sentence of its description, cut at 300 characters
read_onlyWhether 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 with playbooks.
  • With playbook, it returns only the servers that serve that playbook, as the playbook's own route does, with playbooks included.
  • A key limited to some playbooks must pass playbook, and sees in playbooks only the playbooks it may access. Without playbook it is refused with forbidden, whose detail says to name one of its playbooks in playbook.
  • A playbook the org does not have returns not_found, as the playbook's own route does, and no server is asked.
  • A server that no server of the org, or of the playbook given, has returns invalid_input at /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, idempotentHint and openWorldHint), 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.

http
GET /v1/orgs/acme/playbooks/sales/tool-servers/billing/tools/getInvoice
Authorization: Bearer <key>
json
{
  "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.

http
POST /v1/orgs/acme/playbooks/sales/tool-servers/billing/tools/getInvoice/validate
Authorization: Bearer <key>
Content-Type: application/json

{ "arguments": { "id": 42 } }
json
{ "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:

FieldContents
would_runWhether execute would call the tool now: the caller may, the playbook is not retired, the operator allows the tool, and its server responded
needsplaybook:write, the permission execute needs whatever the tool does
caller_mayWhether the caller has that permission
playbook_retiredWhether the playbook is retired, which execute refuses, since a retired playbook can no longer change
allowedWhether the operator allows the tool on its server's entry, for runs and for execute alike
read_onlyWhether 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
argumentsThe 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
serverreachable (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:

Caseintrospect and validatedry_run
A server not set up for the playbookKind tool_not_offered, because: "tool_server_not_configured", before any server is askedThe same
A tool the operator does not allowKind tool_not_offered, because: "tool_not_allowed", before any server is askedReturns allowed: false
A tool the server does not listKind tool_not_offered, because: "tool_not_listed"The same
A connection that is not readyKind tool_not_offered, with its because, as belowThe same, or server.reachable: "no" with because: "connection_not_allowed" for an address its document names
A server that cannot be askedKind tool_server_failed, with its because, as execute returns itReturns 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.

http
POST /v1/orgs/acme/playbooks/sales/tool-servers/billing/tools/getInvoice/execute
Authorization: Bearer <key>
Content-Type: application/json

{ "arguments": { "id": "inv-2041" } }
json
{
  "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"
}
FieldContents
call_idThe id of the call, a UUID, under which its two events are recorded
outcomeHow the call ended, from the table below
textWhat 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
answerThe 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_bytesThe 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_msHow long the call took
server_request_idThe server's own id of the request, when the operator's request_id setting says where the server carries one
called_atWhen the call was made, in ISO 8601 UTC
outcomeMeans
resultThe tool returned a result
tool_errorThe tool returned an error or refused the arguments; a run's model may recover from this
server_failureThe server failed on the call
timed_outThe server did not respond within 30 seconds
too_largeThe 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: unavailable with kind tool_not_offered and because set to tool_not_allowed, tool_server_not_configured or tool_not_listed, or for a connection that is not ready connection_pending, connection_broken or connection_not_allowed;
  • the server cannot be used: unavailable with kind tool_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_runs and get_run_history never show it, while list_playbook_events does.
  • The arguments and the result are kept unless the operator turns record_content off 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}:

OperationMethod and routePermissionInput
add_connectionPOST /connectionsorg:writename, kind, entry, credential
list_connectionsGET /connectionsorg:readOptional include_removed
get_connectionGET /connections/{id}org:readConnection id in path
remove_connectionDELETE /connections/{id}org:writeConnection id in path
read_linkGET /connections/{id}/link/{token}any, with the link's tokenConnection id and token in path
approvePOST /connections/{id}/approveany, with the link's token and the adder's sessiontoken
give_credentialPOST /connections/{id}/credentialany, with the link's token and the adder's sessiontoken, 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.

  • entry holds the server without its credential: url for mcp; document and base_url for openapi; and headers, query_params, allowed, playbooks, record_content and request_id, as an entry of the configuration file has them. url and document are each an https URL, or an http URL on a loopback address.
  • credential is one object, kind and the fields that kind takes, each optional in the schema and refused as invalid_input at the field where its kind lacks it or does not take it: none takes no other field; secret takes name and in, header, query or security, so name is the header, the query parameter or the security scheme of the document the value is sent as, and only an openapi connection takes query or security; reference takes variable, a variable of the runtime such as CRM_API_KEY that the operator lists in connections.variables, whose value is sent where its name and in say, as a secret's; and client_credentials takes issuer, an https URL or an http URL on a loopback address, client_id and an optional scope. 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.
  • approve connects a connection whose credential is none or reference and uses its link up. give_credential keeps 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_connection refuses 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, approve and give_credential need the session of the person who added the connection. In local mode the token alone binds.
  • remove_connection removes 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.
CaseResponse
An entry that holds a value that looks like a credential, a reference such as ${NAME}, or a field another entry would refuseinvalid_input
A kind of credential there is not, or a field its kind lacks or does not takeinvalid_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 namesforbidden, with the sentence of the rule
A name of an entry of tool_servers, or of a connection of the org that is not removedconflict
An org that holds 50 connections that are not removed, the most it mayconflict, kind connections_full
A token that is unknown, used up or expired, or an id the org does not havenot_found
add_connection by a key where people sign in, or by anyone on a runtime of API keys aloneforbidden
approve or give_credential with the session of another person, where people sign inforbidden
approve of a connection whose credential is to be given, or give_credential of one that takes noneconflict

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:

becauseThe connection
connection_pendingNobody has approved it yet; the person who added it opens its link, or removes it and adds it again
connection_brokenIts credential cannot be read, and the runtime recorded it broken, with why in broken; the person removes it and adds it again
connection_not_allowedThe 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_input includes an errors list of JSON pointers.
  • A rejection that has a kind carries it.
  • An unavailable or conflict rejection that has a because carries it.
  • A conflict may have the kind oversized, 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:

json
{
  "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.

StatusCommon reason
400bad_request
401Missing or invalid credentials
403forbidden or origin_not_allowed
404not_found
405Unsupported method; see the Allow header
409conflict or cancelled
410unanswered
411length_required
413content_too_large
415unsupported_media_type
422invalid_input
500internal
503unavailable

A problem document's type is a URI that names its kind of problem:

TypeStatusMeans
https://on.auto/problems/bad_request400The request is malformed, such as a field given twice or a body that is not a JSON object
https://on.auto/problems/unauthenticated401Credentials are missing or invalid
https://on.auto/problems/forbidden403The credentials may not do that
https://on.auto/problems/origin_not_allowed403A browser sent the request from an origin the server does not allow
https://on.auto/problems/not_found404Something the request names does not exist
https://on.auto/problems/method_not_allowed405The path does not take that method
https://on.auto/problems/conflict409The request clashes with what is there
https://on.auto/problems/tools_called409A 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_unknown409A 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/cancelled409The run was cancelled, with the kind of its cancellation; its reason is cancelled
https://on.auto/problems/unanswered410Nobody 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_required411A body a route streams, a dataset or a candidate, was sent without its Content-Length
https://on.auto/problems/content_too_large413The 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_type415The 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_input422The input does not fit, with the errors that point at it
https://on.auto/problems/client_closed_request499The client disconnected before the response
https://on.auto/problems/internal500Something went wrong inside the server
https://on.auto/problems/unavailable503Something the server relies on cannot serve now
https://on.auto/problems/tools_unfinished503A 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/rebuilding503The 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_input is status 422 over HTTP, but a workflow raises it as a validation error of status 400. A catch for it matches status 400 or the validation type, never 422.
  • A cancellation of a called run that the workflow does not catch rejects the workflow as invalid_input, not as cancelled, 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_offered or model_not_offered;
  • with because set to builds_of_the_day, stalled, not_finite or not_in_order, which no wait resolves;
  • with cancelled.

Models and tools a function may not use

CaseResponse
A function names a model outside the deployment's allow listunavailable with kind model_not_offered and because: "model_not_allowed", before the provider is called
A function names a model whose provider is missingunavailable with kind model_not_offered and because: "provider_not_configured"
A function names a tool the playbook's servers do not offerunavailable with kind tool_not_offered
introspect, validate, dry_run or execute names a tool the playbook is not offeredunavailable 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.