---
title: "Agents"
description: "Define an agent in one Markdown file, or in YAML inside a plugin: model, instructions, tools, contracts, limits and delegation."
canonical_url: "https://docs.oz0.ai/build/parts/agents"
---
# 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](https://docs.oz0.ai/concepts/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:

```bash [Terminal]
oz0 agent new triage --uses github --description "Triage new GitHub issues"
```

```markdown [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](https://docs.oz0.ai/reference/plugin-manifest#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](https://docs.oz0.ai/reference/agent-definition#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:

```bash [Terminal]
oz0 plugin eject ./triage.md --out triage
```

```text
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](https://docs.oz0.ai/build/plugins/install/rollouts#new-versions-roll-out-to-canaries-first); 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):

```yaml [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:

<table>
<thead>
  <tr>
    <th>
      Runtime
    </th>
    
    <th>
      How it runs
    </th>
    
    <th>
      Status
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        pydantic-ai
      </code>
    </td>
    
    <td>
      A <a href="https://ai.pydantic.dev" rel="nofollow">
        Pydantic AI
      </a>
      
       agent with Temporal durability: the loop is workflow code and every model call is an activity
    </td>
    
    <td>
      Works today; the default
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        harness:&lt;name>
      </code>
    </td>
    
    <td>
      A whole session of an agent harness, such as <code>
        harness:claude-agent-sdk
      </code>
      
      , run by a harness plugin, with options under <code>
        harness:
      </code>
      
      . See <a href="https://docs.oz0.ai/build/parts/harnesses">
        Harnesses
      </a>
      
      .
    </td>
    
    <td>
      Works today
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        openai-agents
      </code>
    </td>
    
    <td>
      An agent on the <a href="https://openai.github.io/openai-agents-python/" rel="nofollow">
        OpenAI Agents SDK
      </a>
      
      , run as Temporal's own integration runs it: the loop is workflow code, every model call and every tool call an activity
    </td>
    
    <td>
      Works today
    </td>
  </tr>
</tbody>
</table>

## 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.

<table>
<thead>
  <tr>
    <th>
      
    </th>
    
    <th>
      <code>
        pydantic-ai
      </code>
    </th>
    
    <th>
      <code>
        openai-agents
      </code>
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Models
    </td>
    
    <td>
      Every format the gateway speaks: Anthropic's Messages API, OpenAI's Responses and Chat
    </td>
    
    <td>
      OpenAI's Responses API (<code>
        openai/&lt;model>
      </code>
      
      ) and Chat (<code>
        local/&lt;model>
      </code>
      
      , OpenAI-compatible provider records); an Anthropic model is refused
    </td>
  </tr>
  
  <tr>
    <td>
      Aliases (<code>
        model.use
      </code>
      
      )
    </td>
    
    <td>
      Any model the alias resolves to
    </td>
    
    <td>
      The models of the alias that speak an OpenAI format; the job fails with <code>
        ModelFormatUnsupported
      </code>
      
       when none does
    </td>
  </tr>
  
  <tr>
    <td>
      Live stream
    </td>
    
    <td>
      Text as the model writes it
    </td>
    
    <td>
      Text as the model writes it, streamed from the model call
    </td>
  </tr>
  
  <tr>
    <td>
      Hand-offs
    </td>
    
    <td>
      No
    </td>
    
    <td>
      <code>
        handoffs:
      </code>
      
       gives the conversation to another <code>
        openai-agents
      </code>
      
       agent of the plugin, in the same job
    </td>
  </tr>
  
  <tr>
    <td>
      Long conversations
    </td>
    
    <td>
      <code>
        history.continue_after
      </code>
      
       and <code>
        history.compact
      </code>
    </td>
    
    <td>
      <code>
        history.continue_after
      </code>
      
       only; <code>
        compact
      </code>
      
       is ignored, with a warning from lint
    </td>
  </tr>
  
  <tr>
    <td>
      Colleagues, the board, events
    </td>
    
    <td>
      <code>
        oz.ask_colleague
      </code>
      
      , <code>
        oz.board_write
      </code>
      
      , <code>
        oz.board_read
      </code>
      
      , <code>
        oz.wait_for_event
      </code>
    </td>
    
    <td>
      The same tools; a colleague's question is answered from the job's task and what it streamed so far
    </td>
  </tr>
  
  <tr>
    <td>
      Outcome checks and best-of
    </td>
    
    <td>
      <code>
        outcome:
      </code>
      
      , <code>
        best_of:
      </code>
      
       with <code>
        n
      </code>
      
      , <code>
        models
      </code>
      
       or <code>
        agents
      </code>
    </td>
    
    <td>
      The same: the check runs around the SDK's run, and a revision is the next turn of its conversation
    </td>
  </tr>
  
  <tr>
    <td>
      Shared GPUs
    </td>
    
    <td>
      <code>
        model.placement: fleet
      </code>
      
      , <code>
        model.priority
      </code>
    </td>
    
    <td>
      The same: the SDK's model calls go to the model's queue, and the GPU node answers them without the agent
    </td>
  </tr>
</tbody>
</table>

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:

```yaml [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](https://docs.oz0.ai/spend/models-and-keys/aliases#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](https://docs.oz0.ai/build/plugins/structure/lint-rules#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](https://docs.oz0.ai/spend/models-and-keys) and [Local models](https://docs.oz0.ai/fleet/local-models).

## Tools

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

<table>
<thead>
  <tr>
    <th>
      Built-in
    </th>
    
    <th>
      What it does
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        oz.delegate
      </code>
    </td>
    
    <td>
      Hands a task to an agent in <code>
        delegation.allow
      </code>
      
       and waits for its answer. See <a href="https://docs.oz0.ai/agents/delegation">
        Delegation
      </a>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        oz.ask_user
      </code>
    </td>
    
    <td>
      Asks a person a question and waits for the answer. See <a href="https://docs.oz0.ai/agents/asking-people">
        Asking people
      </a>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        oz.ask_colleague
      </code>
    </td>
    
    <td>
      Asks a running agent of the same job tree a question and waits for its answer. See <a href="https://docs.oz0.ai/agents/colleagues">
        Colleagues and the board
      </a>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        oz.board_write
      </code>
      
      , <code>
        oz.board_read
      </code>
    </td>
    
    <td>
      Pin a note or finding to the job tree's shared board, and read it. See <a href="https://docs.oz0.ai/agents/colleagues">
        Colleagues and the board
      </a>
      
      .
    </td>
  </tr>
</tbody>
</table>

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](https://docs.oz0.ai/agents/long-jobs).

### Lazy tool loading

Every function an agent lists is in every model request it makes ([what a definition costs](https://docs.oz0.ai/build/parts/tools/definitions#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:

```yaml [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:

<table>
<thead>
  <tr>
    <th>
      Built-in
    </th>
    
    <th>
      What it does
    </th>
  </tr>
</thead>

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

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](https://docs.oz0.ai/govern/tool-policy) and its conditions, the plugin's grants, the hooks, [approvals](https://docs.oz0.ai/agents/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](https://docs.oz0.ai/build/parts/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](https://docs.oz0.ai/build/parts/tools/definitions#the-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](https://docs.oz0.ai/reference/agent-definition).

## 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](https://docs.oz0.ai/improve/where-a-fix-belongs) has the order, and [When to split an agent](https://docs.oz0.ai/build/parts/when-to-split-an-agent) says when one agent should become two.


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
