Parts

Agent contracts

Declare input and output contracts and review compatibility on upgrades.

For: plugin authors.

Define contracts to validate an agent's input and output, and limits to bound its work and spend. Workflow code enforces these checks independently of whether the agent follows its instructions.

Declare the contract

Every agent says what it takes and what it answers: input: text, or input_schema with a JSON Schema, and the same for its output.

agents/greeter.yaml
input: text
output: text

plugin lint and plugin install warn about an agent that declares neither for its input or its output, and from v1.0 that is an error. A schema's root should be an object, as MCP's tool schemas are, and its $refs must stay inside the file: nothing fetches a remote one at run time, so lint refuses it. plugin new writes input: text and output: text into the agents it creates; for an agent written before, add the two lines, or point at a schema.

The contracts make the runtime API's OpenAPI document, the run forms in the console, and the upgrade check.

A contract has seven parts: the input and the output above, what a person answers when a step pauses, the annotations of its tools, examples, a version and hash, and the failure kinds the agent may end a job with.

Input contract

agents/acme-reviewer.yaml
input_schema: schemas/review-input.json
schemas/review-input.json
{
  "type": "object",
  "required": ["repo", "pr"],
  "properties": {
    "repo": { "type": "string" },
    "pr": { "type": "integer" }
  }
}

A job whose input does not match fails before the agent starts, with InvalidInput, and is not retried. With an input schema, start jobs with JSON: oz0 run acme-reviewer '{"repo": "acme/api", "pr": 42}'.

Output contract

agents/acme-reviewer.yaml
output_schema: schemas/review.json

The model is asked to answer in that structure. An answer that does not match goes back to the model with the reason, at most twice; after that the job fails with a clear error. The job's answer is then JSON that matches the schema, ready for an app to use.

Results drawn as cards

A result that matches its contract is drawn as a card in the console and in chat channels (the console): an agent's or flow's answer against its output_schema, and a tool's structured result against the outputSchema its MCP server lists. The edge checks the result with the same validator as inputs (never in the browser, never fetching a remote $ref) and draws only from data: what the model received is not changed.

The resultThe card
A list of objects, or an object whose one property is a list of objects (as MCP's SDKs wrap a list, {"result": [...]})A table, its columns in the schema's order
An objectKey-values, in the schema's order
An object whose schema says x-oz0-diff: {before: <property>, after: <property>}, with bothA diff: lines for two texts, keys for two objects
Anything elseText: a string as markdown, nothing that can run; another value as JSON

Two keywords shape the card. x-oz0-order lists property names to show first (columns, keys, and a form's fields); without it a card keeps the order the schema is written in. x-oz0-diff marks the pair to compare:

schemas/change.json
{
  "type": "object",
  "x-oz0-diff": {"before": "old", "after": "new"},
  "x-oz0-order": ["path"],
  "properties": {"path": {"type": "string"}, "old": {"type": "string"}, "new": {"type": "string"}}
}

plugin lint refuses an x-oz0-diff that does not name two different properties of the schema, and warns about an x-oz0-order name that is not one. A result over 64 KiB of JSON is not drawn as a card.

Upgrades keep their contracts

Apps call agents through their contracts, so a new version of a plugin may not break them by surprise. When a plugin is updated, the install (in the management API, before anything reaches a node) generates the OpenAPI document of the installed version's agents and flows and of the new version's, and compares them operation by operation:

  • Breaking: an agent or flow that is gone; an input that accepts less (a new required property, a tighter bound, an enum value gone, a type changed, JSON where text was); an output that may hold more or something else (a property that may now be missing, a new enum value, text where JSON was); a new failure kind, which callers' branches do not know, and a kind that asks more of its callers (retryable turned off, partial or person turned on).
  • Not breaking: a new agent or flow, an input that accepts more (a new optional property), an output that promises more, a new description, a failure kind removed or one that asks less of its callers.

A breaking change needs a new major version of the plugin, its first non-zero part going up (1.4.0 to 2.0.0, or 0.2.1 to 0.3.0), or --allow-breaking. Otherwise the install is refused with what broke:

Error: invalid_argument: invalid request: team 0.1.1 breaks the contracts of 0.1.0: helper: input: takes JSON that
matches a schema instead of text; give it a new major version, or install it with --allow-breaking

An install that goes through prints the changes, plugin list and the plugin's page in the console show the latest ones, and the audit log records breaking changes that were let through (plugin.contracts.breaking). The check compares what it can tell apart: a change to a keyword it does not compare, such as anyOf or a $ref, counts as breaking.

  • Agent failure kinds: Interpret typed failures and their retry behaviour.
  • Agent limits: Bound execution, loops, delegation and the whole job tree.
  • Agent budgets: Reserve spending before calls and configure budget exhaustion behaviour.
Previous section links
Copyright © 2026