Skip to content

Latest commit

 

History

History
531 lines (392 loc) · 71.5 KB

reasoning-format.md

File metadata and controls

531 lines (392 loc) · 71.5 KB
description The document format of a reasoning function: its model, input and output contracts, settings and prompt template, and what a run records.

Reasoning function format

A reasoning function is a definition of the type reasoning. Its document names a model, the input and output contracts, the settings and a prompt template. A run renders the prompt from its input, calls the model and records the model's answer. Build your first playbook walks through one with an agent.

Use one where the work needs judgement or language. It is not for reading what a service holds, which is a lookup function's work; for exact arithmetic or reshaping data, a computation function's; for asking a person or having a service act, an interaction function's; or for the order of the work, its branches and its waits, which are the workflow's.

A function document

The source is Markdown: YAML front matter, then a Liquid prompt template. This example reviews a campaign brief and answers JSON: its recommendation and what the brief misses. Its page heads each run with both, and counts the recommendations of each day. Replace the model reference with a concrete id from list_models, or with one the operator of your runtime confirms.

---
description: Reviews a campaign brief against the team's criteria
model: anthropic/claude-sonnet-4-5
config:
  max_output_tokens: 800
input:
  schema:
    type: object
    properties:
      brief: { type: string }
      criteria: { type: string }
    required: [brief, criteria]
output:
  format: json
  schema:
    type: object
    properties:
      recommendation: { type: string, enum: [Ready, Revise] }
      missing: { type: array, items: { type: string } }
    required: [recommendation, missing]
    additionalProperties: false
page:
  data:
    language: typescript
    source:
      events:
        - type: run_succeeded
          subject: reasoning/review-brief
    views:
      reviews: table
    module: |
      export function fold(view: View, event: Event): View {
        const fieldOf = (value: Json | undefined, name: string): Json | undefined =>
          typeof value === 'object' && value !== null && !Array.isArray(value) ? value[name] : undefined;
        const recommendation = fieldOf(fieldOf(event.data, 'output'), 'recommendation');
        const day = event.time?.slice(0, 10);
        if (typeof recommendation !== 'string' || day === undefined) return view;
        const rows = view.reviews?.rows ?? [];
        const counted = rows.some(([at, kind]) => at === day && kind === recommendation)
          ? rows.map((row) => (row[0] === day && row[1] === recommendation ? [day, recommendation, Number(row[2]) + 1] : row))
          : [...rows, [day, recommendation, 1]];
        return { reviews: { columns: [{ name: 'Day' }, { name: 'Recommendation' }, { name: 'Reviews' }], rows: counted.slice(-60) } };
      }
  result: |
    {{ output.recommendation }}{% if output.missing.size > 0 %}: {{ output.missing | join: "; " }}{% endif %}.
  runs: |
    ```chart bars x=Day y=Reviews color=Recommendation
    {{ views.reviews | json }}
    ```
---

{% system %}Review the brief using the supplied criteria. Recommend Ready only when it meets every criterion, and list what it misses, each in a short clause. Do not invent facts.{% endsystem %}
Brief: {{ input.brief }}
Criteria: {{ input.criteria }}

To save it, call create_definition with type: "reasoning", the name review-brief, which its page's filter names, the document as source, and its overview and diagram. A save without a diagram is refused, so give the word none when a picture would add nothing; The overview says how to write both. To run it, call run_definition with the same type and name, and with brief and criteria in the input object. Both operations also need the playbook id, unless the MCP connection is scoped to that playbook.

A function document contains instructions, never model credentials. Each change to the document creates a version. A run uses the latest active version and records it as definition_version; a run cannot select an older version.

Fields

Field Required Value
description No Text of 1 to 1,000 characters. list_definitions and get_definition show it
model Yes A model reference in provider/model form, such as anthropic/claude-sonnet-4-5
config.max_output_tokens No A whole number from 1 to 64,000; 1,024 when left out. The most tokens the answer may use
config.temperature No A finite number: the sampling temperature. Each provider accepts its own range
config.top_p No A finite number: nucleus sampling
config.seed No A whole number of 0 or more, for providers that accept a seed
config.stop_sequences No A list of strings. The answer stops where the model writes one
config.reasoning No The reasoning effort, for models that reason: none, minimal, low, medium, high or xhigh
input.schema No A JSON Schema whose root is "type": "object". Without it, any JSON object is accepted
input.default No Defaults for top-level input fields, as a JSON object. The supplied input's fields override them before validation
output.format No text, the default, or json
output.schema With json A JSON Schema the answer must match; see Answers that are JSON. Not allowed with text
output.shape No The name of an output shape, series, categories, table or text: the answer is JSON of that shape. Not allowed beside output.schema or with text
provider_options No Options for one provider, under its namespace; see Provider options
tools No The tools a run may call, each server/tool or server/*; see Tools
page No The page that presents its runs

Any other key, at any level, is rejected.

How a document is checked

create_definition and update_definition check the whole document, and every run checks it again. A document with problems is rejected with every problem listed under /source:

  • Each problem names its line and, where there is one, a JSON pointer into the front matter, such as Line 4, /config/temperature: Expected number.
  • A problem found twice is reported once. A rejection lists at most 20 problems in line order, then one line that counts the rest, such as Line 23: 5881 more issues are not shown.
  • The front matter is YAML 1.2 with the core schema, so yes and 2026-10-01 stay strings. Anchors, aliases and tags are rejected. A key may appear only once in a mapping, and numbers must be finite. Nesting is limited to 72 levels.
  • Quote a text that holds #, or a comma inside { }, such as description: 'Posts the notes in #deliveries' or { note: 'Costs 1,000 a month' }. Unquoted, YAML reads # as the start of a comment and a comma there, even the one in 1,000, as the start of another key, so the text ends early, and the save does not always say so.
  • input.default must match the input schema for the fields it names. Required fields may be left for the input to supply.
  • A setting in config is checked only against the type and range in the table above. Whether the provider accepts the value becomes clear when a run calls it.
  • Saving calls no model. Whether the provider is configured, and whether it offers the model, becomes clear only when the function runs; see How a run ends.

How a run works

A run takes these steps:

  1. It applies input.default: each top-level field the supplied input leaves out takes its default. It checks the result against input.schema. The input may be at most 256 KiB as JSON and 512 levels deep. A run without input gets {}.
  2. It renders the template. The system block becomes the system instructions, and everything else becomes one user message.
  3. It calls the model, one call at a time, and records each model call in the run's history. A function with tools lets the model call them before it answers; see Tools.
  4. It checks a JSON answer against output.schema, or against the shape output.shape names.
  5. It records the output and a record of the call.

An external agent can pass evidence it collected through its own connections as input. The function itself cannot reach that agent's tools.

Supply a run_id to read a run later or to retry it. The same id and input return a recorded success or invalid_input without calling the model again. After unavailable, conflict or a failure, they run the definition again. A run that called tools is the exception; see When a tool-calling run cannot finish. A run that is still started is not started a second time: it goes on from what it recorded; see A run performed again. Retrying under a run id gives every case. The HTTP reference gives the other rules of the run operations.

The template

The template is Liquid. It reads three variables and nothing else:

Variable Value
input The run's input, a JSON object, after input.default is applied and the result checked
today The date when the run starts, YYYY-MM-DD, in UTC
now The time when the run starts, in ISO 8601, in UTC

Rules for reading values:

  • The template may also read names it creates itself with assign, for, increment or cycle. Any other name is rejected when the document is saved.
  • When the input schema lists its properties and sets additionalProperties: false, input.<field> must be one of them.
  • A field the input does not have stops the run with invalid_input, naming the field and the line.
  • Two places may read a missing field without stopping the run: a condition of if, elsif or unless, and the default filter. So a template can test a field that may be absent: {% if input.vip %}…{% endif %}, {{ input.nickname | default: "friend" }}.
  • Only the input's own fields can be read: input.constructor is a missing field.
  • A list renders as its items with no separators, and an object renders as [object Object]. Use {{ input.account | json }} to render JSON.

The system block

{% system %}You review expenses against the policy of {{ input.company }}.{% endsystem %}
Review this expense: {{ input.expense | json }}
  • The text between {% system %} and {% endsystem %} is sent as the system instructions. Everything outside it is the message, sent as one user message.
  • A template has at most one system block. The block must sit at the top level, outside every other tag, and its tags take no arguments.
  • The template must render some text outside the block.
  • An input that contains {% system %} or {% endsystem %} renders as those literal characters. It never opens or closes the instructions.
  • The template renders in document order, so a value assigned in the block is available after it.

Tags

Available: assign, if with elsif and else, unless, case with when, for with break and continue, cycle, increment, decrement, echo, liquid, raw, comment, # and tablerow.

Not available: include, render, layout and block, since a function is a single document. capture is not available either; use assign with append instead.

Filters

A template may use the filters listed in Templates, the templates guide, the same in every template.

Limits

Limit Value When it is reached
Length of the template 65,536 characters Refused when saved
Names in tags and outputs 1,000 Refused when saved
Time to render 200 ms The run ends as invalid_input
Memory that filters and ranges may charge 5,000,000 characters or items The run ends as invalid_input
Rendered instructions, and rendered message 200,000 characters each The run ends as invalid_input

Names are counted in the text inside {{ }} and {% %}: every variable, property, filter and keyword. A template that nests tags or parentheses more deeply than the engine can parse (about 2,000 levels of tags) is refused when saved. A render limit ends the run as invalid_input because the input is what makes the render grow.

Answers that are JSON

With output.format: json, the answer must match output.schema. With output.shape, it must match the shape's schema and the rules the shape adds, such as the order of a series' points; Output shapes gives them, and the shape's schema is sent to the provider as output.schema would be. The runtime validates the answer against the schema as written, whatever the provider enforced. The provider is sent the portable form of the schema, and the answer is read back into the schema as written. Through a gateway without structured outputs, and for a schema that is not portable, the schema as written is in the instructions instead of sent as a response format. A schema is a JSON Schema document in draft 2020-12. It is read as draft-07 when $schema says so, or when the document uses definitions without $defs.

These rules apply to output.schema and input.schema alike, and a schema that breaks one is refused when saved:

  • At most 65,536 bytes as JSON, at most 64 nested levels of objects and lists, and at most 1,000 values in one enum.
  • No pattern or patternProperties, because a hostile regular expression can stall validation.
  • A format is one of date, date-time, time, email, uri and uuid, which the runtime checks: date, date-time and time as RFC 3339 writes a full date, a date and time and a full time, such as 2026-04-01, 2026-04-01T09:30:00Z and 09:30:00Z; email as the HTML standard defines a valid email address, such as name@example.com; uri as an absolute URI with a scheme, ASCII in the characters RFC 3986 allows, with no white space and each % followed by two hexadecimal digits, such as https://example.com; and uuid in the form of RFC 4122, such as 0f8fad5b-d9cb-469f-a165-70867728950e. An input with a string out of its format is refused as invalid_input at its field, with the form and its example, such as day: expected a date such as 2026-04-01, and an answer with one fails as output_invalid, such as /day: Expected a string in the format date, such as 2026-04-01. A format checks strings alone, so beside another type it takes every value of that type. Any other format is refused, since the runtime would not check it.
  • No if, then, else, contains, minContains, maxContains, dependentRequired, dependentSchemas, dependencies, unevaluatedItems, unevaluatedProperties or additionalItems.
  • No $anchor, $dynamicRef, $dynamicAnchor, $recursiveRef or $recursiveAnchor.
  • $ref may point only to #/$defs/<name> or #/definitions/<name>. not may only be {}. enum and const may contain only strings, numbers, booleans and null. additionalProperties may not be a schema when properties is present.
  • No definition that loops through $ref, allOf, anyOf or oneOf with no property or item in between, such as "a": {"$ref": "#/$defs/a"}. A definition may refer to itself through properties or items, as a tree does.

An answer may nest at most 128 levels deep. When an input or an answer does not match, the rejection reports each distinct issue once. It lists at most 20, then one line that counts the rest.

A schema whose answers cannot be enforced while the model writes them is still saved. Its answers are asked for with the schema in the instructions, as a schema that is not portable is, and checked after the model writes them. The definition carries warnings, one for each part of the schema that keeps its answers from being enforced, at its line and pointer. They are the same whatever the model and the server, and the output shapes carry none. create_definition, update_definition, get_definition and list_definitions show them, at most 100. They are found in the definition's document whenever it is read, so a definition saved by an earlier release shows the warnings of this one. For example:

Line 5, /output/schema: answers to this schema cannot be enforced while a model writes them: 17 fields that may be null or are a choice of types, counted where each definition is used (most 16). Answers are checked after the model writes them. To have them enforced, make some of these fields required and of one type, or split the answer.

The warnings also name the keywords the runtime does not know, which it does not check: they are left out of the portable form, and written in the instructions as they are when the schema is told to the model there. A format the runtime takes is checked against the answer whatever the provider enforced while the model wrote it.

To have the answers enforced, rewrite what a warning names:

A warning names Rewrite it
A value of any type, an object of any keys and values, or a list whose items may be of any type Give the value a type, the object its properties or a schema of its values in additionalProperties, and the list its items
A schema no value matches Leave out the property or the list that takes it, such as one with "items": false
Required names with no schema in properties Give each name a schema in properties, or leave it out of required
allOf, not, or a list of fixed positions (prefixItems) Write the parts of an allOf as one schema, leave not out, and write a list of fixed positions as an object with a property for each position
oneOf or anyOf beside other keywords Move the other keywords into each choice: beside a choice, keep only a description, bounds and a type every choice shares
A count of properties beside optional properties that may be null Leave out minProperties and maxProperties, or make those properties required
An enum that mixes kinds of value Keep to one kind of value in an enum, such as strings
Keywords beside $ref Keep only a description and bounds beside a $ref, and move the other keywords into its definition
A definition that refers to itself Write out each level the answer needs as a definition of its own
Rules of the keys that cannot be put into words Keep to an enum, a type, a description and bounds in propertyNames
Definitions under both $defs and definitions Keep them all under $defs
A count over its most, such as 17 fields that may be null or are a choice of types (most 16) Do what the warning says: make fields required and of one type, nest fewer levels of objects, list fewer enum values, or split the answer
That no answer this schema accepts could be made to test its portable form with Make bounds that contradict each other agree, such as a minLength above the maxLength

Tools

tools lists the tools a run may call, from the tool servers the operator configured for the playbook and the connections people added to its org. Write each as server/tool, or server/* for every tool of a server that the operator allows; a connection is named the same way, by its name:

tools: [graph/search, graph/query, notes/*]

server/* gives every tool the server lists that the server's allowed setting names. When the operator left allowed out, it gives every tool the server lists. A tool written in another form, or listed twice, is refused when the document is saved.

Find the tool names

Do not guess tool names. Ask the runtime:

  1. Call search (MCP, or GET /v1/orgs/{org}/playbooks/{playbook}/tool-servers/search). It returns the servers set up for the playbook and the tools the operator allows on each. It asks each server for its tools, as a run does. When the servers offer many tools, pass a few words describing what the function should do as terms. See Tool servers.
  2. Call introspect to read one tool in full, with the schema of its arguments.
  3. Call the tool to see what it returns before you name it: query_connection calls a tool marked reads, and execute_connection calls a tool whatever its mark, once the person agrees. Each calls the tool once, as a run would, and returns what the run's model would see. The call is recorded in the playbook's history. dry_run reports beforehand what the call would check. See Calling a tool by hand.

Before the model is called

A run checks its tools after it confirms that its model is offered, and before it calls the model. A run whose model is not offered contacts no tool server. A tool that cannot be used ends the run as unavailable, and the model is never called:

Kind because When
tool_not_offered tool_server_not_configured The server is not configured for the playbook
tool_not_offered tool_not_allowed The tool is outside the server's allowed
tool_not_offered tool_not_listed The server does not list the tool
tool_not_offered connection_pending The server is a connection nobody has approved yet
tool_not_offered connection_broken The server is a connection whose server refused its credential, whose credential is gone from the secret store or is not the tokens of a consent, whose consent was revoked or has ended, or whose client is no longer registered; reconnect it
tool_not_offered connection_not_allowed The server is a connection a rule of the operator no longer allows
tool_not_offered connection_unavailable The server is a connection whose credential cannot be read just now; try again later
tool_server_failed unreachable The server cannot be reached, or does not respond in time
tool_server_failed failing The server fails, or refuses to list its tools
tool_server_failed rate_limited The server asks the run to slow down
tool_server_failed key_refused The server did not accept the key the runtime gives it; check that key, do not retry
tool_server_failed scope_lacking The server asks for a scope its credential lacks, which the details name; do not retry until a credential is granted that scope

What the model sees

  • Each tool reaches the model under a unique name made from server__tool, for an MCP server and an HTTP API alike. Characters other than letters, digits and underscores become underscores. A name longer than 64 characters, or one that clashes with another after that change, gets an 8-character hash. A numeric suffix resolves any clash that remains.
  • The tool's description is cut to 4 KiB. Its input schema is sent unchanged.
  • Parts of a tool's input schema that the provider may refuse or not enforce are added to the run's warnings, such as graph__search inputSchema#/properties/query/maxLength. Tools are not sent as strict structured outputs, so the rules for JSON output schemas do not apply to them.
  • The model keeps the function's output format. It may call several tools in one step, and call tools over several steps, before it answers.
  • Each call carries the run id in the request's metadata, under auto.on/run_id.
  • A tool's text content reaches the model as text. Structured content reaches it only when there is no text. Other content reaches it as a one-line placeholder.
  • A result the server marks isError, such as a policy denial, reaches the model as a tool error it may recover from.
  • A server's own instructions never reach the model. Error text is scrubbed of the server's secrets and of minted tokens.

Limits on calls

The runtime checks each call before it sends it:

  • A run makes at most 25 calls; any further call is refused.
  • A call that sends more than 256 KiB of arguments as JSON is refused, the most a run may take as its input. The model reads how large its arguments were, and is told to send less in one call, in parts where the tool takes them.
  • A third call of the same tool with the same arguments is refused, and the model is pointed to the result of the earlier call.

A refused call is not sent and not recorded, and the model is told why. A tool server may take less than a call may send: Auto Browser, the browser of Auto Cloud, refuses a call of more than 64 KiB with HTTP 413, which the model reads as The tool server answered HTTP 413: the request was larger than the server takes.

A model writes a call's arguments as output tokens, so config.max_output_tokens limits them first. A token is about 3 to 4 bytes of English prose, and fewer of code or of text full of quotes and line breaks. At the default of 1,024 tokens, a model writes about 3 to 4 KiB of arguments. To let it write a large argument, such as a whole document, raise config.max_output_tokens to about one token for every 3 bytes: about 22,000 for a document of 64 KiB. At the most it allows, 64,000, a model writes roughly 200 KiB.

When the model reaches config.max_output_tokens while it writes a call, a call whose arguments were cut so that they no longer parse is not sent: the model reads that they did not parse, as any call it cannot make, and the run goes on. A call whose arguments still parse is sent as written, though the model may have been cut off before it finished them. In a function that answers in JSON, the run ends as conflict, kind unworkable, as a JSON answer cut off at max_output_tokens does. Set config.max_output_tokens above what the largest argument needs. Every later step of the run reads those arguments again as input, so a run that writes several large documents uses its input tokens quickly; Write a large document to a service shows a workflow that writes one document a run.

The model reads each tool result in full. A result is cut, with a note of how much was left out, only when it would not fit in the room left for the step that called it:

  • The room starts as the smaller of two figures: the model's context window, as the server's model catalog gives it, and what is left of the run's budget. The step's input and output are taken off it, and the step's calls share the rest equally.
  • Room is counted in the provider's tokens for that step, and converted to bytes at the step's ratio of bytes sent to input tokens counted.
  • A cut result stays valid JSON, with a note asking for fewer rows, fields or depth.
  • When the catalog does not know the window, or the provider reports no token counts, each result is read in full.

A run's model calls use at most the input tokens the operator sets with REASONING_MAX_INPUT_TOKENS, 2,000,000 by default. Before each step, the runtime estimates that step's input from the step before. When the remaining budget would not cover another step after it, that step is the last, and it is made without tools.

Once the run has used all its calls, or the budget would run out, the model answers from what it has, without tools. That last step gives the model the calls so far as text, so every provider accepts it. The tool results sit in a block fenced by a random marker and labelled as the results of the tools the model called. The label tells the model they are data to answer from, not instructions and not the user's words.

A run that calls tools costs more. Every step resends the conversation so far, so a long run can use its whole budget, 2,000,000 input tokens by default. Ask a tool for one page of its data when the whole would be more than the model should read.

Limits on time and failures

Bound Value When it is reached
One call 30 seconds A tool error that counts as a server failure
A 429 from a tool server Waited out when Retry-After asks for at most 10 seconds Otherwise a server failure
Server failures in one run 5 The run ends with because: "server_failed"
One model call 60 seconds plus 25 ms for every token of max_output_tokens The run ends with because: "model_unavailable"
The whole run 10 minutes, or one model call's limit when that is longer The run ends with because: "run_bound"
A tool call in the last step, made without tools None allowed The run ends with because: "no_answer"

Once the run has called a tool, each of these ends it as tools_unfinished or effect_unknown; see When a tool-calling run cannot finish. Before its first call, the run ends as plain unavailable.

A provider may signal that the request is too long: HTTP 413, the error code or type context_length_exceeded, request_too_large or string_above_max_length, or an error message that says prompt is too long, maximum context length or ContextWindowExceededError. The run then ends as conflict with the kind unworkable. Its detail names the signal, how many bytes the tools returned, and the input tokens the provider counted for the last step.

The run's history

Each call adds two events to the run's history, appended as they happen:

  1. tool_call_started, recorded before the call is sent. It has the call's number, the id the model gave it, the server, the tool and the size of its arguments. It has read_only: true when the tool is marked reads, and the SHA-256 digest of the arguments when they are kept.
  2. Then one of:
    • tool_call_answered, when the tool answered: is_error, the size of the result and its digest when it is kept, shown_bytes when the model read less than all of it, the duration and the JSON-RPC id.
    • tool_call_failed, when it did not: because (arguments_refused, server_failure, timed_out, too_large for a result larger than the transport reads, cancelled, or interrupted for a call whose attempt ended before it answered, which the run makes again; see A run performed again), the failure message, the duration and the JSON-RPC id.

Either event carries the server's own request id when the server's request_id setting says where to find it. The arguments and the result are kept in full, scrubbed of secrets, unless the operator sets record_content: false for the server. A run's history, from list_playbook_events given its run_id, shows each one when it is at most 2 KiB, and its size otherwise. get_event reads them in full.

An image a tool answers is kept as an asset of the run, apart from the result, when the result is kept:

  • The runtime records asset_kept on the asset's own stream, assets/{id}. Its made_by.tool_call names the id the model gave the call, the server, the connection where the server is one, and the tool.
  • The kept result names the asset in place of the image's bytes, as { "type": "image", "mimeType": "image/png", "asset": "<id>" }, so the run's history holds no image.
  • get_asset reads the asset by that id, with an address to its bytes. Its model is null.
  • The runtime keeps an image this way when its bytes are a PNG, JPEG, GIF or WebP image no larger than the limit for one image, and the playbook has room for it. The settings of generation functions set both limits, and how long the asset is kept, 90 days by default. Any other image stays in the result as the tool sent it.
  • With record_content: false, the result is not kept, so its images are not kept either.

When a tool-calling run cannot finish

A tool may change something outside the playbook. So a run that called tools and then could not finish ends in one of two ways, depending on what it called:

It called Ending Status What may follow
Only tools marked reads unavailable, kind tools_unfinished 503 A new run, or a workflow's retry on status 503, may do the work again
Any tool not marked reads conflict, kind effect_unknown 409 No retry on status 503 matches it, since that tool may already have changed an external system

Both endings name, in words, the tools the run called. They keep the because field of the bound or failure that stopped the run.

The same run id never runs that work again. Retrying under a run id gives the rule for every run; for a tool-calling run:

  • run_definition with the run_id of a tool-calling run that ended without succeeding returns conflict with the kind tools_called. When every tool it called is marked reads, the rejection also has because: "only_read", and its words say nothing was changed.
  • A run that is still started is not started again: it goes on from what it recorded, and calls again only a tool marked reads whose call has no answer; see A run performed again.

A caller that disconnects before the run returns does not stop the run: it goes on to its end and records it, and a request with its run_id is given what it recorded. Only the server stopping cuts a run off, and that leaves it to be performed again from what it recorded; see A run performed again.

Inspect its history and any external effects before deliberately starting a new run with a new id. Retrying a successful run returns its recorded result without calling tools again.

Setting up tools

Tool access works in every runtime, whether started with npx or Docker, once the playbook has a tool server: one the operator configures under tool_servers, or a connection a person adds to its org. The operator binds each server under tool_servers to an org, and optionally to some of its playbooks. Its allowed setting narrows the server's tools; see the Configuration reference. Connecting an external agent does not give the function that agent's tools, credentials or accounts; see Tool access inside a reasoning function.

How a run ends

Every rejection is a problem document, and is recorded on the run.

What happened Rejection HTTP
The input is not a JSON object, or does not match the input schema invalid_input, with the issues under /input 422
The template reads a missing field outside a condition invalid_input at /input/<field>, with the line 422
A filter rejects an input value, a render limit is reached, or the message renders empty invalid_input, with the line 422
The model refuses the content under its policy invalid_input 422
The provider rejects the model, the schema or a setting conflict, kind unworkable 409
A provider option its gateway does not allow, or a namespace nothing reads conflict, kind unworkable; nothing is called 409
A JSON answer is cut off at max_output_tokens conflict, kind unworkable: by what the run called, what to move into a function of its own, then raise config.max_output_tokens 409
The answer is too large for the 1 MiB a run records conflict, kind unworkable: lower config.max_output_tokens 409
The provider signals the request is too long conflict, kind unworkable 409
The model is outside the operator's allowed models unavailable, kind model_not_offered, because: "model_not_allowed" 503
The provider is not configured, or its certificate is not trusted unavailable 503
The provider rejects the credentials unavailable, with the HTTP status 503
The provider limits the rate of requests unavailable, with the seconds to wait when it said 503
The provider cannot be reached or cannot serve now, or does not respond in time unavailable: try again later 503
A JSON answer does not match the schema, when asked for once more too unavailable, with the first issues: try again 503
A tool cannot be used before the model is called unavailable, kind tool_not_offered or tool_server_failed 503
A run that called tools cannot finish unavailable, kind tools_unfinished, or conflict, kind effect_unknown 503 or 409
The run under the run_id is still started, and its last event is recent conflict, kind taken: the run is still running; see A run performed again 409
The server restarted before the run finished, and the run kept no content to go on from unavailable, because: "restarted_without_content"; once it called a tool, unavailable, kind tools_unfinished, or conflict, kind effect_unknown, with that because 503 or 409
The server restarted while the run called a tool not marked reads conflict, kind effect_unknown, because: "call_outcome_unknown" 409
The caller disconnects before the run returns The run goes on to its end and records it; a request with its run_id is given its result 499

What the rejections say:

  • A provider's rejection names the provider, the HTTP status and what it means. It quotes the provider's own words only where Provider messages allow them.
  • A rejection for a provider that is not configured names that provider, with the configured providers and the aliases. A rejection for an untrusted certificate says that the operator must act. Neither names a setting. When other providers are configured but not this one, the kind is model_not_offered and because is provider_not_configured.
  • Nothing is sent to a model outside the allowed models.
  • A request that is too long is explained under Limits on time and failures. The tool endings are explained under Before the model is called and When a tool-calling run cannot finish.

More rules:

  • A text answer cut off at max_output_tokens succeeds, with finish_reason: "length" in the record.
  • A model call may take 60 seconds plus 25 ms for every token of max_output_tokens: 85.6 seconds at the default 1,024. That time includes up to two retries of a call that failed with HTTP 408, 409, 429 or 5xx, or could not connect.
  • When a provider ignores or changes a setting, the record's warnings say so.
  • A JSON answer that does not match its schema is asked for once more, with its issues, when the run has room left for it, and the rejection of one that misses it again says after one correction: see An answer that misses its schema.
  • A JSON answer whose schema the provider refuses with HTTP 400 or 422 is asked for once more with the schema in the instructions. When the provider refuses that request too, the rejection says the schema was not the cause and to check the model's settings or choose another model; when too little time was left to ask, it is unavailable, saying to try again: see A schema the provider refuses.
  • The definition operations add their own rejections: not_found for a definition the playbook does not have, and conflict for a retired definition or one whose document no longer parses.

What a run records

get_run returns the output of a succeeded run with its record:

Field What it holds
model The model as requested, as resolved through the aliases, and as the provider answered
settings The settings sent, with the default max_output_tokens filled in
output_format text or json
finish_reason, raw_finish_reason Why the answer ended: stop, length, content_filter, tool_calls, error or other; and the provider's own word for it
usage Input tokens (uncached, cache read, cache write), output tokens (text, reasoning) and the total, added up over the run's model calls; null where unknown
response_id The provider's id of the last response; for Bedrock, its request id
cost_usd What the run's model calls cost in US dollars, added up, where the gateway answered a cost; absent when it answered none
warnings What the provider said it ignored or changed, and the parts of a tool's input schema that are not portable to it; at most 20
duration_ms How long the run's model calls took, added up
prompt The rendered instructions and message, and truncated
  • The output and the record may be at most 1 MiB together; see Runs and results. When the prompt does not fit beside the answer, the record keeps the start of the instructions and of the message, and sets truncated: true.
  • The record contains no credentials. It also leaves out the provider options, which the definition already contains.
  • A run rejected after one of its model calls ended keeps a record with only usage, duration_ms and cost_usd, added up over the model calls that ended, and the playbook's analytics count its tokens. That holds for every ending: an answer that does not match the schema or is cut off, content the model refused, a model that kept calling tools in the last step, an answer too large for the 1 MiB, and a run stopped by a call's deadline, by its bound or by failing tool servers. A count the provider did not report is null.
  • A rejection that comes before any model call ended records no usage, since none is known.

The run's model calls

A run calls its model one call at a time. Each call adds two events to the run's history, appended as they happen:

  1. model_call_started, recorded before the call is sent. It has the call's number, counted from 1 across the run, the model asked for, tools_offered, and messages_bytes, the size of the messages the call adds to the conversation: the prompt for the first call, the answers of the tools for a later one, and for the last step of a run that called tools, the conversation told again in one message.
  2. Then one of:
    • model_call_answered, when the model answered: finish_reason and raw_finish_reason, usage, cost_usd where the gateway answered a cost, response_id, duration_ms, request_bytes, the size of the request the call sent, response_bytes, the model as resolved_model and answered_model, the provider's warnings, tool_calls, one for each tool call the model asked for, with its call_id, the tool and server it names, and the digest of its arguments, output_mode: described where the call told the model the schema of a JSON answer in its instructions, as through a gateway without structured outputs, or portable where it sent the provider the portable form of the schema and read the answer back, and repair where a JSON answer that missed its schema was asked for once more: outcome: repaired, the first answer's issues, and the second request's usage, cost_usd and duration_ms. The call's own usage, cost_usd, duration_ms and request_bytes are then those of both requests. It records schema_refusal where the provider refused the request that gave it the schema, with HTTP 400 or 422, and the answer was asked for with the schema in the instructions: outcome: described, and the refused request's status and duration_ms.
    • model_call_failed, when it did not: because (cancelled, content_refused, credentials_rejected, definition_invalid, interrupted, model_not_allowed, output_invalid, provider_not_configured, provider_unavailable, rate_limited, request_too_long or timed_out), the provider, the failure message as detail, duration_ms, and, where they apply, the usage and cost_usd of an answer the run could not use, finish_reason, retry_after_ms, the provider's own words, the issues it named, output_mode, described where the answer it could not use was asked for in the instructions or portable where it was asked for in the portable form, and repair where that answer missed its schema: outcome: invalid with the first answer's issues and the second request's usage, cost_usd and duration_ms when it was asked for once more and missed it again, outcome: failed with because, how the second request failed, such as provider_unavailable, the first answer's issues and duration_ms when that request failed, or outcome: skipped with because, input_tokens, context_window or time, when the run had too little room left to ask; and schema_refusal where the provider refused the request that gave it the schema: outcome: described when the answer asked for with the schema in the instructions failed, refused_again when the provider refused that request too, so the schema was not the cause, or skipped with because: time when too little time was left to ask, each with the refused request's status and duration_ms.

The messages each call adds and the model's response are kept in full, scrubbed of the secrets of the run's tool servers, when every tool server of the run keeps content, as record_content keeps it unless the operator turns it off. A run without tools always keeps them. The events then carry their SHA-256 digests, messages_sha256 and response_sha256, and each tool call the digest of its arguments, arguments_sha256. A call keeps only what it adds, never the whole conversation again, so what a run keeps grows with its length. A run goes on from the scrubbed messages and response whether it keeps them or not, so a secret of a tool server in the prompt or in the model's answer reaches no model call and no tool. A JSON answer is checked against its schema again once scrubbed, and one that no longer matches, such as a number that held a secret, ends the run as an answer that does not match its schema does. A run's history shows each one when it is at most 2 KiB, and its size otherwise. get_event reads them in full.

A gateway that answers what a call cost, in US dollars, in a response header gives the call its cost_usd; Gateway fields names the header. The record of the run adds up its model calls.

A run performed again

A run is not interrupted by its client going away; only the server stopping cuts it off, and that leaves it to be performed again from what it recorded. A run a workflow's step started goes on within that step: it stops when the step's server stops or hands its workflows to another server, and the server that runs the workflow then goes on with it. Nothing limits how many runs go on after their clients went away: each is bounded as any run is, and its client started it.

cancel_run cancels a run that is still started, whether its client is there or gone. How soon the run stops depends on what started it and which server takes the cancel:

  • A run its client started stops at once when the server running it takes the cancel, and each call it has in flight ends as interrupted. A run a workflow's step started stops the same way when that workflow cancels it.
  • A run going in another server, or a run a workflow's step started that cancel_run cancels, stops before its next model call or tool call. A call it already sent still records its answer, and a run whose last model call answers ends with that answer.

Every cancelled run records the spending of its model calls so far. One that does not end with an answer ends rejected with the reason cancelled; see Cancelling a run.

A run that is still started is never started a second time. run_definition with its run_id goes on with it from what it recorded, at the version it started with, and answers when the run ends. The workflow host does the same for the calls its workflows wait on, once a server starts again after one stopped.

A run is told to be still running by its events alone. Every model call and every tool call has a bound: the model call's limit under Limits on time and failures, and the server's bound of a tool call, 30 seconds. The two together are longer than any gap between two events of a run that is still going. A run whose last event is newer than that may still be running, so a request for it is refused as conflict with the kind taken, saying the run is still running, and makes no second attempt. Once its last event is older, the attempt that wrote it has ended, and the request goes on with the run. The age of the last event is read from the database's clock, so two servers that share a database need not agree on the time. The workflow host, which knows the server it replaces stopped, goes on at once. In the server where the run is going, a request for it waits for it to end and is given what it recorded, so two requests for one run in one server make one attempt of it, and a client that sends a request twice is answered once. Each model call and each tool call is numbered, and an event is recorded only under the next number, or as the end of a call that is still open, so of two attempts that go on at once, from two servers too, the second stops at its first event, before it calls anything, and no call is ever started a second time.

The run goes on from its events and the content it kept, and reads them before it does anything else: it takes its prompt from what its first model call kept and does not render the template again, and an ending it meets before its first call, such as a tool server that cannot be opened yet, names the tools it called and records the spending of its model calls, as every other ending does.

  • The bound of a run that calls tools, 10 minutes, counts from its start.
  • A model call that was answered is not made again. A model call started without an answer ends as interrupted and is made again from the same messages, under the next number.
  • When the last model call asked for tool calls, each call that has an answer gives the model that answer again, rebuilt from what the run kept, and a call that failed gives the model its failure. A call without an answer ends as interrupted, and is made again when the tool is marked reads. Every call counts toward the run's 25, the ones made before included.
  • A call without an answer of a tool not marked reads is not made again, since it may already have changed something outside the playbook, and neither is any other call of the same step without an answer, even one of a tool marked reads. Each ends as interrupted, and the run ends as conflict, kind effect_unknown, with because: "call_outcome_unknown".
  • A call of a tool the run no longer offers, because its tool server changed while the run was stopped, is not sent. The model is told so and goes on.
  • A model call that failed ends the run as that failure did, and a run whose cancel was asked ends cancelled, neither calling anything.
  • A run that kept no content, because a tool server of the run has record_content: false, cannot go on. Each of its calls without an answer ends as interrupted, so every call it started has an end, and the run ends as unavailable with because: "restarted_without_content", or, once it called a tool, as tools_unfinished or effect_unknown with that because; see When a tool-calling run cannot finish.

A run that has ended is never performed again in this way; Retrying under a run id gives the rule.

The overview

create_definition and update_definition take an overview beside the document: Markdown for the person who runs the business, written from everything known of them and of the playbook. It says why the function exists, what it does and how it fits, in one to three short paragraphs; less technical, more value. Write it after the document, and write it again with every update.

They also take a diagram, every time, so that whether the function is drawn is a choice its author makes: Mermaid source of a flowchart or a state diagram of how the function works, whose first line is flowchart, graph or stateDiagram, or the word none when a picture would add nothing. A save without it is refused. A diagram holds at most 8,192 bytes in UTF-8, and Auto Studio draws it under the overview. It is read there at full size in a column, so draw it top to bottom, as flowchart TD or a state diagram, in three to eight nodes, each a few plain words for the person who runs the business rather than an expression or code.

An overview holds 1 character to 16,384 bytes in UTF-8. The version keeps it as written, and get_definition returns it so until an update gives another; an update that leaves it out keeps the overview saved. list_definitions leaves it out. Reasoning functions gives the rules every definition follows, with used_by, the definitions of the playbook that use this one.

Its page

A reasoning function's page is the page key of its front matter, written with the document and saved with it. It shows the person the judgment the function made:

  • result: the judgment in a sentence, with the fields that decided it, such as Revise: the audience names no job role. for a review of a brief.
  • runs: the outcomes by category over time, such as the reviews of each day by their recommendation.

Show numbers over time as a chart keyed by the period a run is about, and anything else in its clearest form: a table, a list or a sentence. Show the value of the work, not telemetry: never how long the runtime took, a model's tokens or a run's id.

A page counts what the answer names, so give the answer the fields that decide it: output.format: json, and an output.schema that holds them, such as the recommendation of Ready or Revise and the criteria it found missing in the example. An answer in text gives the page its words alone. Page format says how to write data, result and runs.

Provider options

provider_options passes options to one provider, under the namespace that provider reads. Only options that shape how the model reasons or writes its answer are offered:

provider_options:
  anthropic:
    thinking: { type: enabled, budgetTokens: 2000 }
Namespace Read for models of Offered
anthropic anthropic, bedrock-anthropic, vertex-anthropic thinking, with only type, budgetTokens and display
openai openai, and azure when there are no azure options textVerbosity, reasoningMode, logitBias
azure azure textVerbosity, reasoningMode, logitBias
google google, and vertex when there are no googleVertex or vertex options thinkingConfig, safetySettings, threshold
vertex vertex thinkingConfig, safetySettings, threshold
googleVertex vertex, vertex-anthropic thinkingConfig, safetySettings, threshold, and thinking as for anthropic
amazonBedrock bedrock reasoningConfig
bedrock bedrock when there are no amazonBedrock options, and bedrock-anthropic reasoningConfig

An option is offered only when this table lists it; an option a provider adds is not offered until it is reviewed and listed. Put the options of Anthropic models on Bedrock under anthropic, since Bedrock passes anything under bedrock through unchanged. The runtime checks the option names, including the names inside thinking; the provider checks the values when the run calls it.

These are never offered, for any provider:

  • Anything the front matter sets, such as the effort level (config.reasoning), the token limit or how a JSON schema is sent (output).
  • Credentials, and attribution such as user, metadata or labels.
  • Anything stored or reused on the operator's account: store, previousResponseId, conversation, container, cacheControl, cachedContent, and prompt cache keys and retention.
  • Routing, fallbacks, capacity and service tiers.
  • Request headers and betas, such as anthropicBeta.
  • Tools and servers, such as mcpServers.
  • Raw request fields, such as additionalModelRequestFields or prediction.
  • Options for provider-managed conversation state, since the runtime manages the run's conversation.
  • Output the runtime does not read, such as logprobs or reasoningSummary.
  • Options that change how the runtime sends the prompt, such as systemMessageMode or forceReasoning.

An option that is not offered is refused when the document is saved. The problem names the option and says it is not offered, with the reason, such as effort is not offered: the front matter sets it. A namespace that is neither a provider's nor shaped like a gateway's name (a lowercase letter, then up to 31 letters, digits and hyphens) is refused the same way. Do not copy a provider's full API request into this field; write the options the table offers and read the validation feedback.

Options for a gateway

A gateway is an endpoint the operator configures under its own name. Every field under the gateway's name, or its camel-case form (my-gateway and myGateway), goes into the request body unchanged. user is read from openaiCompatible. A gateway gives meaning to body fields its operator may not want a function to set: attribution and budgets, routing and fallbacks, mock answers, endpoints and credentials. So under any of these namespaces, a function may set only the top-level fields the operator lists in the gateway's allowed_provider_options. By default that list is empty. The operator cannot allow a field the runtime sets, such as temperature.

Saving does not know which gateways are configured, so these checks happen when the function runs:

  • A field outside the list rejects the run as conflict with the kind unworkable, naming the field and saying the gateway does not allow it. The gateway is not called.
  • A namespace shaped like a gateway's name that no configured provider or gateway reads rejects the run the same way, before any provider is called.

The repository's engineering reference explains, for contributors, why each option is withheld.