Agents
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:
oz0 agent new triage --uses github --description "Triage new GitHub issues"
---
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
capabilitynamed after the file (triage), whoseuses: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, or0.0.0-devon 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 newnever 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/v1tells it from a Claude Code subagent, andnamemay 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:
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):
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:
| Runtime | How it runs | Status |
|---|---|---|
pydantic-ai | A Pydantic AI agent with Temporal durability: the loop is workflow code and every model call is an activity | Works 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-agents | An 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 activity | Works 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-ai | openai-agents | |
|---|---|---|
| Models | Every format the gateway speaks: Anthropic's Messages API, OpenAI's Responses and Chat | OpenAI'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 to | The models of the alias that speak an OpenAI format; the job fails with ModelFormatUnsupported when none does |
| Live stream | Text as the model writes it | Text as the model writes it, streamed from the model call |
| Hand-offs | No | handoffs: gives the conversation to another openai-agents agent of the plugin, in the same job |
| Long conversations | history.continue_after and history.compact | history.continue_after only; compact is ignored, with a warning from lint |
| Colleagues, the board, events | oz.ask_colleague, oz.board_write, oz.board_read, oz.wait_for_event | The same tools; a colleague's question is answered from the job's task and what it streamed so far |
| Outcome checks and best-of | outcome:, best_of: with n, models or agents | The same: the check runs around the SDK's run, and a revision is the next turn of its conversation |
| Shared GPUs | model.placement: fleet, model.priority | The 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:
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-in | What it does |
|---|---|
oz.delegate | Hands a task to an agent in delegation.allow and waits for its answer. See Delegation. |
oz.ask_user | Asks a person a question and waits for the answer. See Asking people. |
oz.ask_colleague | Asks 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_read | Pin 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:
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-in | What 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, withInvalidInput. 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.