Parts

Agents

Define an agent in one Markdown file, or in YAML inside a plugin: model, instructions, tools, contracts, limits and delegation.

For: plugin authors.

Define an agent in one Markdown file, or place its definition inside a full plugin. The platform runs each agent as AgentWorkflow on its plugin's task queue for the required labels. Only nodes that have the plugin and match those labels can run it; see routing.

An agent in one file

A first agent is one file: its fields in YAML front matter, and its instructions below. oz0 agent new writes it:

Terminal
oz0 agent new triage --uses github --description "Triage new GitHub issues"
triage.md
---
apiVersion: oz0/v1
description: Triage new GitHub issues
model:
  use: default
  needs: [tools]
input: text
output: text
# Other plugins' functions, by address. Each one listed is granted at install; keep only what it needs.
tools:
  - github/oz0_github.get_issue  # Read
  # - github/oz0_github.add_labels  # Sends out: take out the # to let it call this
limits:
  max_steps: 20
  timeout: 10m
---
You triage new GitHub issues: read the issue, say whether it is a bug or a feature, and why.
  • It is a plugin of its own. There is no manifest to write: the loader makes one, a capability named after the file (triage), whose uses: are the other plugins' parts the agent names by address (A plugin that is one part). Its version comes from where the file came from: the digest of an upload, a Git tag or commit, a workspace revision, or 0.0.0-dev on a dev server.
  • --uses <plugin> lists that plugin's functions by address from your tenant's catalog when your CLI context reaches a server: the Read ones on, the others commented out with their type, so you take the # out of the ones the agent needs. --tool <plugin>/<tool>.<function> lists exactly one function (repeatable). Without a server it writes a commented example. agent new never writes a manifest and never overwrites a file.
  • The front matter takes every field of the YAML form below except instructions, which is the body; apiVersion: oz0/v1 tells it from a Claude Code subagent, and name may be left out (it is the file's name). A Markdown agent has the details.

Install and run it

Run oz0 plugin install ./triage.md in your cluster's tenant. It packs the file alone, without what lies beside it. On a terminal it shows what the agent needs, may do and reaches, and asks once; yes also grants what it uses of other plugins. Then oz0 run triage "Triage issue 12". A script without a terminal takes the defaults, which grant nothing: it is refused with the --accept-uses <digest> to give, so a script's install is two commands. An update is oz0 plugin update triage, which uploads the file again.

Try it on a dev server

Run oz0 dev ./triage.md to install the file and reinstall it on every save; oz0 run ./triage.md "..." runs it once on a dev server and node of its own, which run starts and stops (--with <folder> installs a plugin it uses from its folder first). run takes its argument as a file or folder when it exists and is written as a path (./triage.md, ./triage, triage/); anything else is an agent's name or address, even where a folder of that name exists, such as the one plugin eject writes. Write ./<folder> to run a folder.

When it needs more: eject

When the agent needs code of its own (a tool), evals or more parts (a second agent, a flow, a skill), oz0 plugin eject writes the full plugin folder it stands for:

Terminal
oz0 plugin eject ./triage.md --out triage
triage/
├── oz0-plugin.yaml      the manifest: the name, kind and uses: the loader made, and the version kept
├── agents/triage.yaml   the front matter's fields as you wrote them, with instructions: prompts/triage.md
├── prompts/triage.md    the body
└── evals/triage.yaml    one case to fill in: an install runs the evals on a test node first

The plugin keeps its name and its version (the installed one's, when your CLI context's server has it; --version sets another). Installed over the plugin, oz0 plugin install ./triage updates it, with its grants, its history and its rollouts; its version moves when you change version: in oz0-plugin.yaml. A part that wants a version of its own is a one-file plugin of its own, not a second agent in the first one's folder.

An agent in a plugin

Inside a full plugin, an agent is a YAML file (or a Markdown agent) listed under agents: in the manifest (a file, or a folder of them):

agents/acme-reviewer.yaml
name: acme-reviewer                  # unique per tenant; jobs address agents by name
description: Reviews pull requests.  # shown to agents that may delegate to this one
runtime: pydantic-ai
model:
  use: default                       # one of the tenant's aliases: default, fast or smart
  needs: [tools]                     # what the alias's model must be able to do
instructions: prompts/acme-reviewer.md # the system prompt, a path in the plugin
tools: [acme_github.get_pr, acme_github.comment, oz.delegate]
input_schema: schemas/review-input.json    # optional JSON Schema for the job's input
output_schema: schemas/review.json         # optional JSON Schema for the answer
requires:
  labels: {gpu: "true"}              # only nodes with these labels run it
limits:
  max_steps: 20                      # model calls per job (default 50)
  max_tokens: 200000                 # input plus output tokens per job
  timeout: 10m                       # s, m or h
delegation:
  allow: [acme-explainer]            # agents oz.delegate may start
  max_depth: 1                       # how deep a chain of delegations may go
  max_children: 3                    # delegations per job

Runtime

runtime says how the agent runs:

RuntimeHow it runsStatus
pydantic-aiA Pydantic AI agent with Temporal durability: the loop is workflow code and every model call is an activityWorks today; the default
harness:<name>A whole session of an agent harness, such as harness:claude-agent-sdk, run by a harness plugin, with options under harness:. See Harnesses.Works today
openai-agentsAn agent on the OpenAI Agents SDK, run as Temporal's own integration runs it: the loop is workflow code, every model call and every tool call an activityWorks today

Choosing a framework

Both frameworks run in the node runtime, as workflow code, so a job keeps going from its last step after a node or a worker restarts, and both get what the platform adds to every agent: tools from plugins on the right node, oz.delegate and oz.ask_user, hooks and approvals on every call, default-deny of undeclared tools, contracts, limits, budgets, quotas and kill switches, the live stream, the job's trace, continue-as-new for long agents and shadow runs. An agent on one framework delegates to an agent on the other like to any agent: the child is a job of its own.

pydantic-aiopenai-agents
ModelsEvery format the gateway speaks: Anthropic's Messages API, OpenAI's Responses and ChatOpenAI's Responses API (openai/<model>) and Chat (local/<model>, OpenAI-compatible provider records); an Anthropic model is refused
Aliases (model.use)Any model the alias resolves toThe models of the alias that speak an OpenAI format; the job fails with ModelFormatUnsupported when none does
Live streamText as the model writes itText as the model writes it, streamed from the model call
Hand-offsNohandoffs: gives the conversation to another openai-agents agent of the plugin, in the same job
Long conversationshistory.continue_after and history.compacthistory.continue_after only; compact is ignored, with a warning from lint
Colleagues, the board, eventsoz.ask_colleague, oz.board_write, oz.board_read, oz.wait_for_eventThe same tools; a colleague's question is answered from the job's task and what it streamed so far
Outcome checks and best-ofoutcome:, best_of: with n, models or agentsThe same: the check runs around the SDK's run, and a revision is the next turn of its conversation
Shared GPUsmodel.placement: fleet, model.priorityThe same: the SDK's model calls go to the model's queue, and the GPU node answers them without the agent

Pick pydantic-ai unless you want the SDK's hand-offs or already think in its terms. The example plugin examples/openai-desk currently requires repository access; public source downloads are coming soon. It has an SDK agent that hands off to a second one, delegates to a Pydantic AI agent and calls an MCP tool:

agents/triage.yaml
name: openai-desk-triage
runtime: openai-agents
model:
  primary: openai/gpt-5.5
instructions: prompts/triage.md
input: text
output: text
tools: [oz.delegate, openai_desk.hello]
handoffs: [openai-desk-billing] # SDK hand-offs: openai-desk-billing takes the conversation over, in this job
delegation:
  allow: [openai-desk-writer, openai-desk-billing] # a hand-off target must be allowed too
  max_depth: 2                       # how long a chain of hand-offs may be
  max_children: 3                    # delegations and hand-offs together

A hand-off is a delegation within the job: its target must be in delegation.allow, a chain of hand-offs is at most max_depth long, and hand-offs count against max_children with the job's delegations. The target calls its own tools under its own name, so hooks, approvals and kill switches for it apply, and its model calls are metered under its name in the job of the agent that handed off. Its answer is the job's answer, so it answers with the output contract of the agent the job started on, not its own; plugin lint warns when the two differ.

What the SDK offers beyond that, such as its guardrails, sessions, hosted tools and agents as tools inside one run, is not part of an agent's YAML: an agent is data, and tools are plugins.

Model

Ask for an alias. A plugin that names a vendor's model runs only for tenants who pay that vendor. Every tenant has the standard aliases default, fast and smart, seeded from its providers, so ask for one of them with what the agent needs: model: {use: default, needs: [tools]} for most agents, fast for short, frequent jobs, smart for the hardest ones.

A tenant that installs the plugin maps any other alias you ask for at install (plugin install --map).

Name a vendor's model only when the agent needs that vendor's own feature, and say which in model.vendor_reason; a verified plugin fails plugin lint --verified without it (OZ-S016).

Models are named <provider>/<model>: anthropic/claude-sonnet-5-5, openai/<model>, any provider record of the tenant's, or local/<model> for a model server on the node. Calls go through the node's local LLM proxy to the edge's gateway, which adds the tenant's key. Nodes never hold provider keys. fallback models are tried in order when the primary fails.

Instead of a vendor's model, an agent may ask for one of the tenant's aliases with what it needs, model: {use: smart, needs: [tools], min_context: 128k}, which the edge resolves when a job starts; and data: local keeps an agent on local models only. An agent on local/<model> with model.placement: fleet runs anywhere and sends its model calls to the GPU nodes that serve the model to the fleet, waiting model.wait for a slot before its fallback; model.priority (1 first, 5 last) orders its calls among the fleet's. See Models and providers and Local models.

Tools

tools lists <tool>.<function> names from the tenant's plugins, and built-ins:

Built-inWhat it does
oz.delegateHands a task to an agent in delegation.allow and waits for its answer. See Delegation.
oz.ask_userAsks a person a question and waits for the answer. See Asking people.
oz.ask_colleagueAsks a running agent of the same job tree a question and waits for its answer. See Colleagues and the board.
oz.board_write, oz.board_readPin a note or finding to the job tree's shared board, and read it. See Colleagues and the board.

An agent can call only the tools it lists, and only those its job's home node has installed: every other call is refused, and the model reads why, such as The call to acme_crm.refund was not made: it is not among the tools this agent declares. Hooks and approvals then decide each call that is allowed. Tools run on the job's home node unless their affinity is any.

An agent that runs for hundreds of steps sets history: to keep its conversation and its Temporal history bounded. See Long jobs.

Lazy tool loading

Every function an agent lists is in every model request it makes (what a definition costs). An agent over a plugin with many functions, such as a plugin made from an API with hundreds of operations, can load them when it needs them instead:

agents/clerk.yaml
tools: [office.create_issue, office.read_file, office.send_invoice]   # and the rest
tool_loading: lazy

Its model then gets two built-in tools in place of those definitions, the same two in every request of the job:

Built-inWhat it does
oz.find_tools(query, limit)Searches the functions this agent may call now, by the words of their names and descriptions, and answers with up to limit of them (5 unless it says, at most 20): each one's name, the first sentence of its description and its input schema.
oz.call_tool(name, arguments)Calls one of them by name with its arguments.

Nothing about a call changes. The platform unwraps oz.call_tool before anything decides it, so the call is the real function's: the agent must list it, the tenant's tool policy and its conditions, the plugin's grants, the hooks, approvals and kill switches decide it, and the audit log, the job's trace and the live feed name the real function.

The search never offers a function the agent may not call: what the tool policy refuses, a routine does not list, a grant does not reach, a kill switch stops or the node's tool snapshot does not have is left out before anything is ranked. It is a plain keyword search on the node: no model, no embeddings, nothing sent anywhere.

Use it when an agent lists more functions than a model needs to see at once, roughly when its definitions pass a few thousand tokens. Keep it eager when the agent uses most of its few tools in every job: the model then takes a step to search before each new tool, and the search's answer is in the conversation instead. Built-ins (oz.delegate, oz.ask_user and the others) always stay as they are.

Lazy loading applies to pydantic-ai and openai-agents agents, and to ACP harness agents, whose session's MCP server then offers the two tools; a JSON-lines harness and a remote agent run their own tools, so lint warns that it does nothing there.

Contracts

  • input_schema: a job whose input does not match fails before the agent starts, with InvalidInput. With an input schema, the job's input is JSON.
  • output_schema: the model answers in that structure, and an answer that does not match goes back to it with the reason, at most twice.

Schemas are JSON Schema files in the plugin. See Contracts and limits.

Limits

max_steps, max_tokens and timeout end the job when reached. The platform enforces them; the model cannot ignore them. tool_tokens is the agent's size budget for its tool definitions, which lint, test and pack check.

Where it runs

requires.labels limits the agent to nodes with those labels. The catalog shows which nodes are ready: oz0 catalog.

Every field

See the agent definition reference.

When an agent gets a job wrong

Keep the job as an eval case, then put the fix in the cheapest part that holds it: a skill, a tool, a tool's manual, delegation, a hook or an outcome check before more instructions. Where a fix belongs has the order, and When to split an agent says when one agent should become two.

Copyright © 2026