> ## Documentation Index
> Fetch the complete documentation index at: https://docs.auto.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent File

> Field-by-field reference for .auto/agents/*.yaml — every field, type, default, and constraint the agent schema defines.

Every agent in a project is declared as a YAML document under `.auto/agents/`. This page is the exhaustive reference for that document: every field, its type, whether it is required, its default, and the constraints the schema enforces at apply time. For the conceptual tour of what an agent is, start with [Agents](/concepts/agents); for the full trigger grammar, see [Triggers](/reference/triggers).

## File layout and compilation

Agent files live at `.auto/agents/*.yaml` (`.yml` and `.json` are also accepted). A single file may contain multiple YAML documents separated by `---`; each document compiles into one agent. There is no `kind:`/`metadata:`/`spec:` envelope — fields sit at the root of the document, and legacy envelopes are rejected with an error pointing at the facade format.

Reusable fragments live under `.auto/fragments/` and are pulled in with `imports:`. Legacy directories are rejected outright: `.auto/sessions/`, `.auto/environments/`, and `.auto/identities/` all fail apply with an error telling you to move the content into `.auto/agents` (environments and identities are inline-only now).

Compilation follows a fixed order for each document:

1. Merge each entry in `imports:` in listed order (later imports win per-field).
2. Apply `remove:` directives against the merged import result.
3. Merge the document's own fields last — the document always wins.

After the merge, the compiled spec is validated. Three things are required for the result to be a runnable agent: `name`, `harness`, and `environment`. Everything else is optional or defaulted. Imports typically supply `harness` and `environment`, which is why a thin agent file that consists of a `name` and an import is common:

```yaml .auto/agents/pr-review.yaml theme={null}
name: pr-review
imports:
  - "@auto/agents@latest/pr-review.yaml"
  - ../fragments/environments/agent-runtime-base.yaml
variables:
  repoFullName: fractal-works/auto
  githubConnection: github-fractal-works
```

<Note>
  Unknown root-level keys in an agent document are silently ignored — the compiler only routes keys it knows about. A typo'd top-level field name does not fail apply; read the applied spec back with the `auto.resources.get` tool when a field seems to have no effect. Strict unknown-key rejection does apply *inside* structured fields such as `model`, `session`, and `triggers`.
</Note>

## Metadata

<ParamField path="name" type="string" required>
  The agent's resource name, unique within the project. Trimmed, 1–128 characters, matching `[A-Za-z0-9_.-]+`.

  The name `default` is special: an agent named `default` receives Auto's built-in Default base before its own imports, so `name: default` alone is a complete, runnable agent.
</ParamField>

<ParamField path="labels" type="map of string to string">
  Free-form labels attached to the agent resource. Keys must be non-empty.
</ParamField>

<ParamField path="annotations" type="map of string to string">
  Free-form annotations, same shape as `labels`.
</ParamField>

```yaml theme={null}
name: chief-of-staff
labels:
  purpose: chief-of-staff
```

## Authoring controls

These fields steer compilation and are stripped before the spec is validated — they never appear in the applied resource. Full semantics live in [Imports and fragments](/reference/imports-and-fragments) and [Variables and templating](/reference/variables-and-templating).

<ParamField path="imports" type="string or string[]">
  Files to merge beneath this document, in order. Also spelled `import`; when both are present, `imports` wins. Two path forms are accepted:

  * A **relative path** to another file in the bundle, resolved against the importing file's directory. Absolute paths and URLs are rejected ("Agent import must be a relative path").
  * A **managed-template specifier** — `@scope/name@version/subpath` (for example `@auto/agents@latest/pr-review.yaml`). The version defaults to `@latest` when omitted; the file subpath is required. See [Managed templates](/reference/managed-templates).

  Each imported file must contain exactly one YAML or JSON document. Import cycles fail compilation with the cycle path named.
</ParamField>

<ParamField path="variables" type="map of name to string | number | boolean">
  Values substituted into `{{ $name }}` tokens in **imported content only** — the declaring document's own body is never substituted. Names must match `[A-Za-z_][A-Za-z0-9_]*`; numbers and booleans are coerced to strings. Declared only on the entry document; the scope flows down the whole import tree.

  Runtime tokens without a `$` (such as `{{ github.review.htmlUrl }}`) never match and pass through untouched. When [GitHub Sync](/concepts/github-sync) applies the file, it supplies context variables (for example `repoFullName` and `githubConnection`) beneath your declared map — a declared value always wins.
</ParamField>

<ParamField path="remove" type="map of target to string or string[]">
  Deletes named items from the merged import result before this document's own fields merge on top. Supported targets are exactly `tools`, `triggers`, and `env`; any other target fails with "Unsupported agent remove target". Names key into the tool alias, the env var name, and for triggers the item's `name`, else its `event`, else its comma-joined `events`, else `cron:<cron>:<timezone>` with the timezone exactly as authored — empty when the trigger declares none (`cron:0 8 * * *:`), since the `UTC` default is applied only at validation, after merging. Mounts are deliberately not removable.
</ParamField>

```yaml theme={null}
name: quiet-reviewer
imports:
  - ../fragments/reviewer-base.yaml
remove:
  triggers:
    - github.pull_request.reopened
  env: DEBUG_MODE
```

### Merge semantics per field

When multiple imports (or an import and the document body) set the same field, the winner depends on the field family:

| Fields                                                                                                                                                                              | Merge behavior                                                                                                                        |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `name`, `labels`, `annotations`, `harness`, `model`, `reasoningEffort`, `displayTitle`, `session`, `spendCaps`, `concurrency`, `replace`, `manages`, `bindings`, `workingDirectory` | Records deep-merge key-wise; scalars and arrays override.                                                                             |
| `systemPrompt`, `initialPrompt`, `onReplace`                                                                                                                                        | Plain values override; an `append` directive concatenates (see below).                                                                |
| `environment`, `identity`                                                                                                                                                           | Inline objects deep-merge across imports; a string reference overrides an object and vice versa.                                      |
| `env`, `tools`                                                                                                                                                                      | Named maps: keys deep-merge; removable via `remove`.                                                                                  |
| `mounts`                                                                                                                                                                            | Named array keyed by `name`, else `mountPath`: matching items deep-merge, new items append. Not removable.                            |
| `triggers`                                                                                                                                                                          | Named array keyed by `name` → `event` → joined `events` → `cron:<cron>:<tz>`: matching items deep-merge, new items append. Removable. |

### File-backed strings and `append`

`systemPrompt`, `initialPrompt`, and `onReplace` accept three value forms:

* A plain string.
* `file: <relative path>` — the content of a file, resolved against the declaring file's directory. Absolute paths and URLs are rejected; a missing file fails compilation.
* `append: <string>` — exactly one key with a string value. Concatenates onto the imported base with your exact whitespace (no hidden separator). An `append` that never finds a base fails at compile time; a plain value merging over a pending append fails with an error telling you to import the base document before its append overlay.

```yaml theme={null}
name: pr-review-strict
imports:
  - "@auto/agents@latest/pr-review.yaml"
systemPrompt:
  append: |

    House rule: flag any new dependency added to package.json and ask
    the PR author to justify it before approving.
```

## Harness and model

<ParamField path="harness" type="enum" required>
  The agent runtime. One of `claude-code` or `codex`. Required after merge ("Agent requires harness") — typically supplied by an imported fragment or template.
</ParamField>

<ParamField path="model" type="object">
  Model selection. Strict object; when present, `model.id` is required. Omitting `model` entirely uses the harness default: `claude-code` runs `claude-opus-4-8` on `anthropic`, `codex` runs `gpt-5.6-sol` on `openai`.

  <Expandable title="model fields">
    <ParamField path="model.provider" type="enum">
      One of `anthropic`, `openai`, `openrouter`. Defaults to the harness default provider. Each harness accepts a fixed provider list: `claude-code` accepts only `anthropic`; `codex` accepts `openai` and `openrouter`.
    </ParamField>

    <ParamField path="model.id" type="string" required>
      The model id, trimmed, 1–256 characters. Must be in the curated list for the resolved provider, or match the provider's open pattern.

      Curated `claude-code`/`anthropic` ids: `claude-opus-4-8` (default), `fable`, `claude-fable-5`, `claude-opus-4-7`, `claude-opus-4-6`, `claude-sonnet-5`, `claude-sonnet-4-6`, `claude-haiku-4-5`, `claude-haiku-4-5-20251001`.

      Curated `codex`/`openai` ids: `gpt-5.6-sol`, `gpt-5.5`, `gpt-5.3-codex`.

      `codex`/`openrouter` ids are open (any `vendor/model` slug such as `z-ai/glm-5.2`); the live service checks the slug against OpenRouter's catalog where network access is available.
    </ParamField>

    <ParamField path="model.openrouter" type="object">
      OpenRouter routing preferences. Valid only when the resolved provider is `openrouter` (codex-only today). Mutually exclusive with `model.fallbacks`.

      * `models` (required): 1–3 fallback model slugs. The primary id is prepended automatically at the gateway, so entries must not include it and must not repeat.
      * `provider` (optional, strict): OpenRouter provider routing preferences — `allowFallbacks` (boolean), `order` (string array, min 1), `sort` (`price` | `throughput` | `latency`), `only` (string array, min 1), `ignore` (string array, min 1).
    </ParamField>

    <ParamField path="model.fallbacks" type="object[]">
      Same-provider model fallback chain: 1–2 entries of `id` (required, 1–256 chars) and optional `provider`. Each entry resolves through the same harness rules as the primary; its provider defaults to the primary's and must equal it. Ids must be distinct from the primary and from each other, and the declared `reasoningEffort` must be valid for every fallback. Mutually exclusive with `model.openrouter`.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField path="reasoningEffort" type="enum">
  One of `minimal`, `low`, `medium`, `high`, `xhigh`, `max` — validated per harness and model. `claude-code` accepts `low` through `max` and defaults to `high`. `codex` accepts `minimal` through `xhigh` and defaults to `medium`, with per-model narrowing for OpenRouter models (for example `z-ai/glm-5.2` accepts only `high` and `xhigh`; `x-ai/grok-4.5` accepts `medium` and `high`).
</ParamField>

<CodeGroup>
  ```yaml codex on OpenAI theme={null}
  harness: codex
  model:
    provider: openai
    id: gpt-5.6-sol
  reasoningEffort: xhigh
  ```

  ```yaml codex via OpenRouter theme={null}
  harness: codex
  model:
    provider: openrouter
    id: z-ai/glm-5.2
    openrouter:
      models:
        - moonshotai/kimi-k2.7-code
      provider:
        sort: throughput
  reasoningEffort: high
  ```

  ```yaml claude-code with fallback theme={null}
  harness: claude-code
  model:
    id: fable
    fallbacks:
      - id: claude-sonnet-5
  ```
</CodeGroup>

## Prompts and display

<ParamField path="systemPrompt" type="string">
  The agent's system prompt. Trimmed, 1–100,000 characters. Accepts the `file:` and `append:` forms described above.
</ParamField>

<ParamField path="initialPrompt" type="string">
  The first message delivered when a trigger spawns a session. Trimmed, 1–20,000 characters. Renders `{{ … }}` tokens against the normalized event payload at spawn time — `{{ github.pullRequest.number }}`, `{{ chat.channelId }}`, `{{ message.text }}`. Tokens with a `{{ payload.… }}` prefix are rejected at apply time with a message telling you to drop the prefix (that prefix is valid only on mount `ref` templates). Accepts `file:` and `append:`.
</ParamField>

<ParamField path="displayTitle" type="string">
  Session title shown in listings. Either the literal `infer` (the platform infers a title) or a template string, trimmed, 1–20,000 characters, rendered against the event payload with the same no-`payload.`-prefix rule as `initialPrompt`.
</ParamField>

```yaml theme={null}
displayTitle: "Chief of Staff"
initialPrompt: |
  {{message.author.userName}} mentioned you on Slack.

  Trigger context:
  - Channel: {{chat.channelId}}
  - Thread: {{chat.threadId}}
  - Message text: {{message.text}}
```

## Environment

<ParamField path="environment" type="string or object" required>
  The sandbox runtime the agent's sessions run in. Required after merge ("Agent requires environment"). Either the name of an environment generated elsewhere in the bundle, or an inline object — the compiler turns an inline object into a generated environment resource and stores only its name on the agent. Identical inline environments that share a name across files dedupe; different content under the same name fails apply ("Conflicting generated resource").

  The inline object carries the environment's metadata (`name`, required; optional `labels` and `annotations`) alongside its spec fields. The spec is strict. Full details on presets, setup caching, and build behavior are on [Environments](/reference/environments).

  <Expandable title="environment fields">
    <ParamField path="environment.name" type="string" required>
      Resource name for the generated environment: 1–128 characters, `[A-Za-z0-9_.-]+`.
    </ParamField>

    <ParamField path="environment.image" type="object" required>
      The base image: `kind: preset` (the only kind) plus a preset `name` (for example `node24`).
    </ParamField>

    <ParamField path="environment.env" type="map">
      Environment variables baked into the runtime. Same shape as the agent-level [`env`](#environment-variables-env) field, including `$secret` references.
    </ParamField>

    <ParamField path="environment.resources" type="object">
      Runtime sizing: `cpuCount` (integer, min 1) and/or `memoryMB` (integer, min 128). At least one must be present when the object is declared.
    </ParamField>

    <ParamField path="environment.steps" type="string[]" default="[]">
      Image build steps (for example `RUN apt-get install …`), executed when the image is built.
    </ParamField>

    <ParamField path="environment.setup" type="object[]" default="[]">
      Named setup steps run in the sandbox before the session starts. Each entry: `name` (resource name), `commands` (array of non-empty strings, min 1), and optional `cache` — `key` (resource name), `files` (relative paths, no traversal), `paths` (absolute paths under `/home/user`).
    </ParamField>

    <ParamField path="environment.setupCache" type="object">
      Setup-result cache TTL: `ttl`, a duration string matching `[1-9][0-9]*(s|m|h|d)` — for example `30m`, `24h`, `7d`.
    </ParamField>

    <ParamField path="environment.approvals" type="enum" default="bypass">
      Harness approval posture: `bypass` auto-approves every harness action (the sandbox is the isolation boundary); `prompt` opts back into the harness's own approval escalation. Consumed by the `codex` harness only today — `claude-code` always bypasses, so `prompt` under `claude-code` is a no-op.
    </ParamField>
  </Expandable>
</ParamField>

```yaml .auto/fragments/environments/agent-runtime-base.yaml theme={null}
harness: claude-code
environment:
  name: agent-runtime
  labels:
    purpose: agents
  image:
    kind: preset
    name: node24
  resources:
    memoryMB: 8192
  steps:
    - RUN apt-get update && apt-get install -y --no-install-recommends postgresql-client redis-tools jq file && rm -rf /var/lib/apt/lists/*
    - RUN npm install -g tsx
```

## Environment variables (`env`)

<ParamField path="env" type="map" default="{}">
  Environment variables injected into the agent's sessions. Keys must match `[A-Za-z_][A-Za-z0-9_]*`. Each value is either a plain string or a secret reference:

  * `$secret: <name>` — resolves a project [secret](/reference/secrets) at launch.
  * `optional: true` — when the secret does not exist, the variable is omitted instead of failing the session launch, and apply-time validation does not require the secret to be set.

  Merged as a named map across imports; individual variables are removable via `remove.env`.
</ParamField>

```yaml theme={null}
env:
  NODE_ENV: production
  HERENOW_API_KEY:
    $secret: herenow-api-key
    optional: true
```

## Identity

<ParamField path="identity" type="string or object">
  How the agent presents itself on provider surfaces (Slack app, GitHub attribution, Telegram bot). Either the name of an identity resource generated elsewhere in the bundle, or an inline object; an inline identity without an explicit name inherits the agent's name. The object is strict and requires at least one field. Full realization flow (connecting the identity to providers) is on [Identity](/reference/identity).

  <Expandable title="identity fields">
    <ParamField path="identity.displayName" type="string">
      Human-facing display name. Trimmed, 1–80 characters.
    </ParamField>

    <ParamField path="identity.username" type="string">
      Handle-style username. Trimmed, 1–80 characters.
    </ParamField>

    <ParamField path="identity.avatar" type="object">
      Avatar image: `asset` (required) is a relative path under `.auto/assets/` ending in `.png`, `.jpg`, or `.jpeg` — no absolute paths, drive letters, or `..` segments. Optional `sha256` (64 lowercase hex chars) pins the stored content hash; when the apply carries the asset's bytes, the server re-derives and overwrites it, and a declared hash without a local file resolves only an already-stored or catalog avatar.

      The asset file itself must be a non-empty PNG or JPEG up to 2 MiB, square, between 512×512 and 2000×2000 pixels — the strictest provider bound (Slack app icons), so any accepted avatar can be realized everywhere.
    </ParamField>

    <ParamField path="identity.description" type="string">
      Short description shown on provider profiles. Trimmed, non-empty, and at most 140 characters *as Slack counts them*: a non-ASCII character such as an em dash costs 6, an emoji costs 12, and `"`, `\`, `/` each cost 2, because Slack measures the JSON-escaped form.
    </ParamField>
  </Expandable>
</ParamField>

```yaml theme={null}
identity:
  displayName: Chief of Staff
  username: chief
  avatar:
    asset: .auto/assets/chief-of-staff-engineers.png
  description:
    Auto's Chief of Staff - give @chief a task list; it dispatches coding
    agents, shepherds them to green, and reports back.
```

## Mounts

<ParamField path="mounts" type="object[]" default="[]">
  Repositories checked out into the sandbox. The only mount kind is `git`. Mounts are merged across imports by `name`, else `mountPath`, and are deliberately **not** removable via `remove`. Full capability semantics and the credential flow are on [Mounts](/reference/mounts).

  <Expandable title="mount fields">
    <ParamField path="mounts[].kind" type="string" required>
      Always `git`.
    </ParamField>

    <ParamField path="mounts[].repository" type="string" required>
      The repository, for example `owner/repo`. Trimmed, non-empty.
    </ParamField>

    <ParamField path="mounts[].mountPath" type="string" required>
      Absolute path in the sandbox where the checkout lands. Must start with `/`.
    </ParamField>

    <ParamField path="mounts[].ref" type="string">
      Branch, tag, or ref expression to check out. May contain `{{ path.to.value }}` tokens rendered against the session's run input — the `{ triggerEventId, payload }` wrapper — so this is the one surface where a `payload.` prefix is correct (for example `{{payload.github.pullRequest.number}}`). Sessions whose input cannot resolve a template ref are refused at spawn time.
    </ParamField>

    <ParamField path="mounts[].depth" type="integer">
      Shallow-clone depth. Positive integer.
    </ParamField>

    <ParamField path="mounts[].auth" type="object" default="{ &#x22;kind&#x22;: &#x22;none&#x22; }">
      Mount authentication. `kind: none` for public clones, or `kind: githubApp` to mint scoped GitHub App installation tokens. With `githubApp`:

      * `installationId` (optional string) pins a specific installation.
      * `commitAuthor` (optional): `name` (non-empty) and `email` (non-empty, must contain `@`) stamped on commits.
      * `capabilities` (optional): per-capability level `none` | `read` | `write` (merge is `none` | `write` only). Defaults: `contents: write`, `pullRequests: write`, `issues: write`, `checks: read`, `actions: read`, `workflows: none`, `secrets: none`, `merge: none`. `merge: write` requires both `contents: write` and `pullRequests: write` — GitHub has no standalone merge permission, so an incoherent grant is rejected at apply time. The `secrets` capability grants the GitHub Actions secrets API; read grants listing secret names, never values.
    </ParamField>
  </Expandable>
</ParamField>

```yaml theme={null}
mounts:
  - kind: git
    repository: fractal-works/auto
    mountPath: /workspace/auto
    ref: main
    depth: 1
    auth:
      kind: githubApp
      capabilities:
        contents: read
        pullRequests: write
```

## Tools

<ParamField path="tools" type="map of alias to tool" default="{}">
  Tools exposed to the agent's sessions, keyed by alias. An alias is a resource name (1–128 chars, `[A-Za-z0-9_.-]+`); the alias `workspace` is reserved by Claude Code. Every tool variant additionally accepts `disabled: true` to keep the declaration in place while switching the tool off. Variants are discriminated on `kind`; the full runtime behavior of each is documented on [Tools](/reference/tools).

  <Expandable title="tool variants">
    <ParamField path="tools.<alias> — kind: mcp_remote" type="object">
      A remote MCP server. Fields:

      * `url` (required): the server URL; must be `https:`.
      * `transport` (optional, default `streamable_http`): the only supported value.
      * `description` (optional): non-empty string.
      * `auth` (optional, default `kind: none`): one of `kind: none`; `kind: bearer` with `token: { $secret: <name> }`; `kind: mcp_oauth` with `connection: <name>` and optional `optional: true`; or `kind: provider_oauth` with `provider`, `connection`, and optional `optional: true`.
    </ParamField>

    <ParamField path="tools.<alias> — kind: connection" type="object">
      A hosted provider-backed tool (Slack, Linear, Notion, and other built-in providers): `provider` (string, 1–64 chars), `connection` (resource name), optional `description`, optional `optional: true`.
    </ParamField>

    <ParamField path="tools.<alias> — kind: local, implementation: auto" type="object">
      The Auto coordination tool (spawn, message, bind, introspection — see [auto tools](/runtime/auto-tools)). Optional `description`; optional `capabilities` with `billing: none | read | write` (default `none`).
    </ParamField>

    <ParamField path="tools.<alias> — kind: local, implementation: chat" type="object">
      The chat tool for posting to connected chat providers. `auth` is **required**: either `kind: connection` with one `provider` + `connection` pair, or `kind: connections` with a `connections` array (min 1) of such pairs; both accept `optional: true`.
    </ParamField>

    <ParamField path="tools.<alias> — kind: local, implementation: ping" type="object">
      A connectivity-test tool. Optional `description` only.
    </ParamField>

    <ParamField path="tools.<alias> — kind: github" type="object">
      The brokered [GitHub MCP](/runtime/github-mcp) surface. Optional `description`. Optional `tools` (array, min 1) names exactly which GitHub MCP tools to expose, validated against the pinned server's catalog plus the proxy-implemented tools; omitting `tools` applies the curated 24-tool default allowlist. Merge tools (`merge_pull_request`, `enable_pull_request_auto_merge`) additionally require a mount with `merge: write`, and secrets tools require the mount `secrets` capability, even when named explicitly. Auth is not configurable — the proxy mints installation tokens from the session's GitHub App mounts.
    </ParamField>
  </Expandable>

  Merged as a named map across imports; individual tools are removable via `remove.tools`.

  If a referenced connection has no active grant, apply fails — unless the tool declares `optional: true`, in which case apply silently skips it and re-activates it on the next apply once the connection exists.
</ParamField>

```yaml theme={null}
tools:
  auto:
    kind: local
    implementation: auto
    capabilities:
      billing: write
  chat:
    kind: local
    implementation: chat
    auth:
      kind: connections
      connections:
        - provider: linear
          connection: linear
        - provider: slack
          connection: slack
  linear:
    kind: mcp_remote
    description: Linear issues, projects, teams, comments, and product planning.
    url: https://mcp.linear.app/mcp
    auth:
      kind: mcp_oauth
      connection: linear-workspace
  notion:
    kind: connection
    provider: notion
    connection: notion
  github:
    kind: github
```

## Triggers

<ParamField path="triggers" type="object[]" default="[]">
  Events that wake the agent. Each entry is either an **event trigger** or a **heartbeat trigger** (`kind: heartbeat`). This section summarizes the shape; the complete grammar — every field, the `where` filter operators, routing semantics, and cross-field validation rules — is on [Triggers](/reference/triggers), and the event vocabulary is in the [event catalog](/reference/events/github).

  Shared fields on both shapes:

  * `routing` (required): `kind: spawn` (new session per event, optional bind-at-spawn), `kind: deliver` (deliver into existing sessions, with `routeBy` and `onUnmatched: drop | warn | error | spawn`), or `kind: bind` (resolve the bound session for a `target` — one of `github.pull_request`, `github.issue`, `slack.thread`, `agent.singleton`, `linear.issue`, `auto.session` — with `onUnmatched`, optional `release`, and optional `observedTarget`).
  * `where` (optional, default empty = match all): a map of payload path to clause. Paths are a single bare key or `$.dot.separated.segments`; clauses are a bare scalar for equality or exactly one of `contains`, `exists`, `in`, `notIn`, `changedTo`.
  * `message` (optional, 1–20,000 chars): template rendered against the event payload and delivered to the routed session.
  * `attachedUserPrompt` (optional, 1–20,000 chars, non-whitespace): extra user prompt; spawn-routing triggers only.
  * `checks` (optional): GitHub check runs the session must complete; legal only on `github.pull_request.*` events. Each check has `name`, `displayName`, `description`, optional `instructions`, and optional `timeout` / `beginTimeout` / `completeTimeout` objects (`seconds` 1–604,800, `conclusion: success | failure | skipped`; `timeout` and `beginTimeout` are mutually exclusive).
  * `fallback` (optional, default `false`): fires only when no non-fallback trigger on the same endpoint and event key matched.
  * `name` (optional): authoring-only merge key, stripped at compile time.

  Event triggers add:

  * `event` or `events` (exactly one required): the event key or a list of unique keys; a list expands into one stored trigger per event.
  * `connection` (optional): the connection the trigger is bound to.
  * `optional` (optional boolean): apply silently skips the trigger when the referenced connection has no active grant; it re-activates on the next apply.
  * `endpoint` and `auth` (optional): custom-webhook ingress key and its verification (`kind: hmac_sha256 | bearer_token` with `secretRef`, or `kind: none`).

  Heartbeat triggers (`kind: heartbeat`) replace the event fields with:

  * `cron` (required): a cron expression, 1–512 chars.
  * `timezone` (optional, default `UTC`): 1–128 chars.

  `event`, `connection`, `optional`, `endpoint`, and `auth` are forbidden on heartbeat triggers.
</ParamField>

<CodeGroup>
  ```yaml spawn on PR open theme={null}
  triggers:
    - event: github.pull_request.opened
      connection: github-fractal-works
      where:
        $.github.repository.fullName: fractal-works/auto
      routing:
        kind: spawn
        bind:
          target: github.pull_request
  ```

  ```yaml daily heartbeat theme={null}
  triggers:
    - kind: heartbeat
      cron: 0 8 * * *
      timezone: America/Los_Angeles
      routing:
        kind: spawn
  ```

  ```yaml deliver to the slot session theme={null}
  triggers:
    - event: chat.message.mentioned
      connection: slack
      routing:
        kind: deliver
        onUnmatched: spawn
  ```
</CodeGroup>

## Session policy

<ParamField path="session" type="object" default="{}">
  Per-session lifecycle policy. Strict object.

  <Expandable title="session fields">
    <ParamField path="session.archiveAfterInactive" type="object">
      Auto-archive idle sessions: `seconds`, an integer from 60 to 31,536,000 (365 days).
    </ParamField>

    <ParamField path="session.observeSpawnedSessions" type="boolean" default="true">
      Whether this agent's sessions observe the sessions they spawn (receiving their binding lifecycle events). Absent means `true` at the read boundary.
    </ParamField>
  </Expandable>
</ParamField>

```yaml theme={null}
session:
  archiveAfterInactive:
    seconds: 86400
  observeSpawnedSessions: true
```

## Spend caps

<ParamField path="spendCaps" type="object">
  USD spending limits for the agent. Strict object; all fields optional. Each value is a **nonnegative decimal string** (not a number) with at most 10 integer and 10 fractional digits, matching the platform ledger's precision; trailing zeros are canonicalized away. Enforcement behavior is described in [Runtime controls](/reference/runtime-controls).

  <Expandable title="spendCaps fields">
    <ParamField path="spendCaps.dailyUsd" type="string">
      Cap across all of the agent's sessions per UTC calendar day.
    </ParamField>

    <ParamField path="spendCaps.monthlyUsd" type="string">
      Cap per UTC calendar month.
    </ParamField>

    <ParamField path="spendCaps.maxPerSessionUsd" type="string">
      Cap per individual session.
    </ParamField>
  </Expandable>

  Daily and monthly windows reset at UTC calendar boundaries.
</ParamField>

```yaml theme={null}
spendCaps:
  dailyUsd: "25"
  monthlyUsd: "300"
  maxPerSessionUsd: "10.50"
```

## Concurrency and replacement

<ParamField path="concurrency" type="integer">
  Cap on live sessions for the agent. **Only `1` is accepted** — a concurrency-1 agent keeps at most one live session (the agent slot); larger pools are a deliberate non-goal. Declaring any other value fails apply with a message saying so.
</ParamField>

<ParamField path="replace" type="string">
  The literal `auto`, or absent. `replace: auto` asserts the agent's state is externally reconstructable, so the platform may stop a stale or failed slot session and spawn a replacement on the latest spec. Requires `concurrency` to be set ("`replace` requires `concurrency`").
</ParamField>

<ParamField path="onReplace" type="string">
  The rebuild-on-wake prompt delivered to a platform-spawned replacement session. Trimmed, 1–20,000 characters; accepts `file:` and `append:` forms. Requires `replace: auto` — nothing else consumes it.
</ParamField>

<ParamField path="manages" type="string[]">
  Agent-type names whose sessions this agent may manage (stop) within its project, max 64 entries. Authority is granted by agent type, not spawn provenance — a replacement session of a managing agent controls sessions its predecessor spawned. Self-control is always implicit, and unknown names are inert rather than invalid.
</ParamField>

```yaml theme={null}
concurrency: 1
replace: auto
manages:
  - staff-engineer
  - staff-engineer-codex
  - chief-of-staff
onReplace: |
  You are a fresh session replacing a predecessor. Rebuild state from
  external sources (session lists, Slack threads, PR bindings) before
  doing anything else, then resume normal orchestration.
```

## Bindings

<ParamField path="bindings" type="map of target to policy" default="{}">
  Per-target-type binding policy, keyed by binding target type (`github.pull_request`, `github.issue`, `slack.thread`, `linear.issue`, `auto.session`). `agent.singleton` is rejected as a key — the pool slot has its own release lifecycle. Each entry is strict.

  <Expandable title="binding policy fields">
    <ParamField path="bindings.<target>.lifecycle" type="enum" default="manual">
      `manual` — the holding session may release the binding via the agent tools. `held` — manual unbind is rejected; only a platform release, an operator override, or a takeover by a replacement session releases it. `held` requires at least one bind-routed trigger on the same target with `release: true`, so the platform has a way to release it.
    </ParamField>

    <ParamField path="bindings.<target>.continuity" type="enum" default="session">
      `session` — the binding is released when its holder session archives. `agent` — the binding is an agent-level claim that survives archive and rolls to a replacement session; it ends only via explicit unbind, trigger release, or operator authority.
    </ParamField>

    <ParamField path="bindings.<target>.bind" type="enum">
      Declarative auto-bind. `onAttributedEvent` (legal only for `github.pull_request`): when an inbound event carries a PR attributable to one of this agent's sessions, ingestion binds it to that session. `onMention` (legal only for chat-thread targets — `slack.thread` today): after the router delivers an addressed `chat.message.mentioned` event, the delivered session becomes the binding owner. Absent means no auto-bind.
    </ParamField>

    <ParamField path="bindings.<target>.context" type="object">
      JSON object stamped as relationship context on bindings the platform creates for this target.
    </ParamField>

    <ParamField path="bindings.<target>.eventContext" type="object">
      Strict object of default annotations for emitted binding transitions: optional `bound`, `updated`, and `unbound` JSON objects.
    </ParamField>
  </Expandable>
</ParamField>

```yaml theme={null}
bindings:
  github.pull_request:
    continuity: agent
    context:
      role: human-review-shepherd
  auto.session:
    continuity: agent
```

## Working directory

<ParamField path="workingDirectory" type="string">
  The directory sessions start in inside the sandbox. Trimmed, non-empty. Typically points at a mount path, for example `/workspace/auto`.
</ParamField>

## Minimal agent

After all imports merge, a compilable agent needs exactly three things: `name`, `harness`, and `environment`. Everything else defaults — `env: {}`, `mounts: []`, `triggers: []`, `session: {}`, `bindings: {}`, `tools: {}`. An agent with no triggers is valid; it can only be started manually or by another agent.

The shortest valid agent file is one line, because Auto's built-in Default base supplies harness and environment for the reserved name:

```yaml .auto/agents/default.yaml theme={null}
name: default
```
