<!--
Sitemap:
- [Installation](/installation)
- [Upgrading](/upgrading): Version-specific steps for upgrading an existing Bento install.
- [Concepts](/concepts)
- [Build your first pipeline](/tutorials/pipeline-args)
- [Target a specific issue or PR from a URL](/tutorials/url-targeting)
- [Keep state across runs](/tutorials/pipeline-state)
- [Fire a pipeline on a schedule or on demand](/tutorials/schedule-and-fire)
- [Configuration](/configuration)
- [Knowledge Base](/knowledge-base/)
- [Method & delivery](/knowledge-base/modes)
- [Config](/knowledge-base/config)
- [MCP](/knowledge-base/mcp)
- [Pipeline configuration reference](/pipelines/config)
- [Filters](/pipelines/filters)
- [Triggers](/triggers/)
- [GitHub Trigger](/triggers/github)
- [Linear Trigger](/triggers/linear)
- [Webhook](/triggers/webhook)
- [Schedule Trigger](/triggers/schedule)
- [Manual Trigger](/triggers/manual)
- [Traces](/pipelines/traces)
- [Slack](/integrations/slack)
- [Public Access](/public-access)
- [Context Engineering](/context-engineering)
- [Best Practices](/best-practices)
- [Troubleshooting](/troubleshooting)
- [Architecture](/architecture/vision)
- [Workspaces](/workspaces)
- [Authentication](/authentication)
- [Identity](/identity)
- [Security](/security)
- [References](/references)
- [Changelog](/changelog): Bento release history.
- [CLI Reference](/cli/)
- [Setup](/cli/setup)
- [Lifecycle](/cli/lifecycle)
- [Sandbox Image](/cli/image)
- [Observability](/cli/observability)
- [Diagnostics](/cli/diagnostics)
- [Triggers](/cli/triggers)
- [Workbench](/cli/workbench)
- [Auth](/cli/auth)
- [Knowledge](/cli/knowledge)
- [Bento](/index)
- [Runtime Wrapper](/architecture/runtime-wrapper)
- [Skill Evolve](/architecture/skill-evolve)
-->

# Pipeline configuration reference

Each file under `.bento/pipelines/` defines one pipeline: a configuration that connects a trigger to an agent, an optional skill, an `instructions` directive, and an output sink.

## Pipeline structure

Minimal pipeline — a scheduled notification with no event payload:

```yaml
# .bento/pipelines/morning-standup.yaml
trigger:
  schedule: "0 9 * * 1-5"
agent: notifier
instructions: |
  Post a short summary of yesterday's merged PRs and open review requests.
output:
  slack:
    bot: standup
    channel: "#standup"
```

Full-featured pipeline — a webhook trigger with filtering, skill, guardrails, sandbox mounts, and orchestrator override:

```yaml
# .bento/pipelines/pr-review.yaml
trigger:
  github:
    - pull_request.opened
    - pull_request.synchronize
  filter:
    repos: [my-org/backend]
    mentioned: false
    not:
      comment.user.login: my-bot
agent: reviewer
skill: agentic-review
targets: [codex-high, claude-high]
instructions: |
  Review PR #{{event.pull_request.number}} ("{{event.pull_request.title}}")
  on {{event.repository.full_name}}, opened by
  @{{event.pull_request.user.login}}, for merge readiness.
output:
  github: review
guardrails:
  timeout: 900
sandbox:
  backend: podman
  mounts:
    - host: ~/.config/gh
      container: /home/node/.config/gh
    - host: ~/.gitconfig
      container: /home/node/.gitconfig
orchestrator:
  strategy:
    type: auto
context:
  diff: true
```

## trigger

What causes the pipeline to fire automatically. Declare at least one of `github`, `linear`, `webhook` (see [Webhook Trigger](/triggers/webhook)), or `schedule`. `schedule` may be declared alongside event triggers; the other three are event arms and can also be combined. Omit `trigger` entirely for a pipeline that only ever runs on demand — every pipeline is fireable via `bento trigger fire` regardless of what it declares here. Add `patterns` (a list of URL prefixes) to route a pasted URL to this pipeline — see [URL routing](/triggers/manual#url-routing).

### github

A list of GitHub webhook event entries. Each entry is either a bare event type string or a per-event object with its own predicate. The pipeline fires when any entry matches the arriving event and passes the `filter`.

```yaml
trigger:
  github:
    - pull_request.opened
    - pull_request.synchronize
    - pull_request_review_comment.created
    - pull_request_review.submitted
    - issue_comment.created
    - issues.assigned
    - deployment_status
```

Event strings follow the pattern `<event_type>.<action>`. The event type comes from GitHub's `X-GitHub-Event` request header; the action from the `action` field in the payload body.

The object form adds a per-event `when`/`not` predicate and inline `options` overrides evaluated only when that entry fires:

```yaml
trigger:
  github:
    - pull_request.opened
    - event: pull_request.review_requested      # object form
      when: { requested_reviewer.login: ava-lh }
      skip_reviewed_head: false                 # overrides pipeline-level options
  filter:
    when:
      pull_request.draft: false                 # ANDs with per-event when
```

The per-event `when`/`not` AND-s with the shared `filter.when`. Entries are evaluated in declared order; the first whose event type matches and whose predicate passes wins, and its inline options override the pipeline-level `options` for that dispatch only. If one matching entry's predicate fails, evaluation continues to later entries for the same event type. The pipeline does not fire only when no matching entry predicate passes.

### linear

A list of Linear webhook event strings.

```yaml
trigger:
  linear:
    - Issue.create
```

### schedule

A 5-field cron pattern: `minute hour day month weekday`.

```yaml
trigger:
  schedule: "0 9 * * 1-5"   # 09:00 Monday–Friday
```

```yaml
trigger:
  schedule: "0 3 * * *"     # 03:00 daily
```

```yaml
trigger:
  schedule: "*/5 * * * *"   # every 5 minutes
```

Schedule triggers have no incoming event payload. `{{event.*}}` substitutions in `instructions` are not available — the directive is used as-is.

Two fields apply only to `schedule` triggers:

| Field | Description |
|-------|-------------|
| `repo` | Repository to clone before the run, as `owner/name`. Must match the `url` field (not the map key) of a `repos:` entry in `daemon.yaml`. |
| `branch` | Branch to check out. Defaults to the repo's configured branch. |

```yaml
trigger:
  schedule: "0 3 * * *"
  repo: my-org/backend
  branch: main
```

Omitting `repo` means no checkout — the agent runs without a codebase.

### filter

Narrows which events fire the pipeline. All specified fields must match.

| Field | Type | Description |
|-------|------|-------------|
| `when` | map | Payload predicate; all entries must match. Keys are dot-path expressions into the payload object. Values use the leaf vocabulary: scalar = strict equality, array = OR (matches any member), `{ exists: bool }` = presence test. A missing payload path fails a scalar/array match and satisfies `{ exists: false }`. |
| `not` | map | Inverse predicate — rejects the event if ANY entry matches. Same leaf vocabulary as `when`. |
| `mentioned` | boolean | `true` — bot was @-mentioned in the triggering text. `false` — bot was not mentioned. Omit to match either. |
| `repos` | list | Event must come from one of these repositories (`owner/repo` format). |
| `assignee` | string | Event's assignee field must equal this value. |

```yaml
filter:
  when:
    review.state: approved
    pull_request.user.login: my-bot
  not:
    review.user.login: my-bot
    comment.user.login: my-bot
  mentioned: true
  repos: [my-org/backend, my-org/frontend]
  assignee: bot
```

## agent

**Required.** The name of the agent persona to run. Must match a directory under `agents/<name>/` containing a `PERSONA.md` file.

```yaml
agent: reviewer
```

## skill

**Optional.** The name of a skill to give the agent. Must match a directory under `skills/<name>/` containing a `SKILL.md` file. Multiple pipelines can reference the same skill with different agents.

```yaml
skill: agentic-review
```

## companions

**Optional.** Additional skills mounted alongside the governing [`skill`](#skill) as read-only reference. Each must match a directory under `skills/<name>/`. Requires a governing `skill`. The governing skill stays the authoritative procedure; companions carry no pointer, and the runtime surfaces them on relevance through its own skill discovery.

Delivery is mount-only. A run that declares companions on a runtime or backend that cannot bind-mount skills — a non-`claude` runtime, or a remote/host backend — is refused, not run without them.

```yaml
skill: solve-linear-issue
companions: [linear]
```

## version

**Optional.** SemVer string identifying this pipeline's procedure revision, surfaced in the attribution footer of everything it posts.

When the pipeline governs exactly one skill (a `skill` with no `companions` and no orchestrator capabilities), `version` may be omitted — the daemon inherits the skill's own declared version. For every other pipeline — multi-skill, companion-bearing, orchestrator-enabled, or skill-free — `version` must be declared explicitly. Validation fails a pipeline that is ambiguous and undeclared.

```yaml
skill: agentic-review
version: 1.4.2
```

## principals and principal

**Optional.** `principals` binds pipeline-owned role names to canonical provider principals. A value is one `<provider>.principal.<id>` reference or a list containing at most one reference per provider. `principal` selects the binding used by a direct actor run.

```yaml
skill: agentic-review
principals:
  pull-request-author: github.principal.your-org-author-agent
  pull-request-reviewer: github.principal.your-org-review-agent
principal: pull-request-reviewer
```

A skill can declare several required binding names in its `SKILL.md` frontmatter:

```yaml
principals:
  - pull-request-author
  - pull-request-reviewer
```

Every required name must exist in the pipeline. A direct `principal` must select a role declared by the pipeline skill or a companion. A capability `principal` must select a role declared by that capability's skill. Skills and companions name roles only; the pipeline supplies concrete provider references. Generic orchestrator tasks inherit the pipeline's selected role; model-created task input cannot replace it. Observer and dry-run execution cannot select or receive a principal. The legacy `github.username` configuration supplies one implicit GitHub principal only for pipelines without explicit bindings.

## env

**Optional.** The environment variables injected into this pipeline's run sandbox (BIP-20). This manifest is the single, operator-authored declaration of what a run receives. Each entry is either a variable name — forwarded from the daemon's environment — or a computed `{name, command, ttl?, required?}` object whose value is minted by a host command at spawn time.

```yaml
skill: solve-linear-issue
companions: [linear]
env:
  - LINEAR_API_KEY    # forwarded from the daemon environment
  - name: NPM_TOKEN   # minted at spawn time
    command: vault kv get -field=token secret/ci/npm
    ttl: 3600
    required: true
```

For a computed entry, `command` runs on the daemon host — the same trust domain as `setup:` steps — and its trimmed stdout becomes the value; empty output is a failure, not an empty value. `ttl` caches the minted value in memory, keyed by (pipeline, name), for that many seconds; omit it (or set `0`) to mint every run. `required: true` fails the invocation before the agent spawns if the mint fails; without it, a failed mint withholds the variable and the run proceeds.

Validated at startup: every name entry must be set in the daemon's environment; a computed entry is minted at runtime so it skips that check, but must carry a non-empty `command`. The manifest must cover every variable the run's skill, agent, or companions declare in their own `env:` (a declared need the manifest omits is a config error, named at boot). Injection is actor-mode only — a non-empty manifest on a `read_only` pipeline is rejected, since observer runs receive no secrets.

## targets

**Optional.** An ordered list of names from the daemon's [`targets`](/configuration#targets) registry. The first target is primary. A target's `credential_pool` entries are tried in order before later targets when the agent CLI reports a quota or authentication failure. Omit the field to use the agent's frontmatter runtime and model with the runtime's implicit host login.

```yaml
agent: reviewer
targets: [codex-high, claude-high]
```

Bento preflights authentication and tries the primary first. It advances within the same run only when the agent CLI reports a quota or authentication failure. A Claude session-limit result cools that credential down until its reported UTC reset time, so concurrent runs skip it. A transient server-side failure (Anthropic 529 overloaded, dropped API socket) retries the same candidate in-process up to two times before stopping — it does not advance to the next candidate. Infrastructure and unclassified failures stop the candidate loop immediately. Authentication failure removes later candidates that use the same runtime and credential. The pipeline's `guardrails.timeout` is one deadline shared by all attempts; a fallback receives only the remaining time.

## target\_routes

**Optional.** A map of GitHub label name to an ordered target list, used to pick the specialist route by a label on the triggering PR. When the PR carries a label that is a key here, that route's targets are used in place of [`targets`](#targets) — resolved identically (first primary, rest fallbacks). The first key in declaration order the PR carries wins; a PR matching no key falls back to `targets`.

```yaml
agent: reviewer
targets: [codex-high, claude-high] # default: no matching label
target_routes:
  tier:low: [claude-sonnet-high]
  tier:high: [claude-opus-high, codex-high]
```

Keys are arbitrary label names, so a pipeline can route on any label family (`tier:*`, `priority:*`, …) — the daemon has no built-in label vocabulary. Integer-like keys (a label named `1`) are rejected at startup, since JavaScript enumerates them numerically rather than in declaration order. Route targets are validated at startup for target-name, credential, and runtime-auth references, the same as `targets`. The label set comes from the webhook payload's `pull_request.labels` plus the triggering `label`.

## instructions

**Required.** The user message sent to the agent. Supports `{{event.*}}` template substitutions, where `event` is the full webhook payload object, and `{{args.*}}` for declared [inputs](#args). `{{lists.<name>}}` expands a named list from the daemon's [`lists`](/configuration#lists) config (items joined by newlines).

For schedule triggers, no event data is available — use a static directive or declared `args:` defaults.

Examples:

```yaml
# PR review
instructions: |
  Review PR #{{event.pull_request.number}} ("{{event.pull_request.title}}")
  on {{event.repository.full_name}}, opened by
  @{{event.pull_request.user.login}}, for merge readiness.
```

````yaml
# Inline review comment
instructions: |
  A reviewer left a comment on a pull request.

  Repo:     {{event.repository.full_name}}
  PR:       #{{event.pull_request.number}} — {{event.pull_request.title}}
  Reviewer: @{{event.comment.user.login}}
  File:     {{event.comment.path}}:{{event.comment.line}}

  Diff hunk:
  ```
  {{event.comment.diff_hunk}}
  ```

  Comment:
  {{event.comment.body}}
````

```yaml
# Issue assignment
instructions: |
  You were assigned to an issue.

  Repo:   {{event.repository.full_name}}
  Issue:  #{{event.issue.number}} — {{event.issue.title}}
  Author: @{{event.issue.user.login}}
  URL:    {{event.issue.html_url}}

  Body:
  {{event.issue.body}}
```

## args

**Optional.** Typed inputs the pipeline accepts from synthesized surfaces (manual / url / schedule). Each input is keyed by name with a `type` and optional `required` / `default`.

| Field | Type | Description |
|-------|------|-------------|
| `type` | `string` | `number` | `boolean` | The input's value type. `number`/`boolean` coerce from string inputs; anything unrecognized is treated as `string`. |
| `required` | boolean | When `true`, the input must be supplied or the fire is rejected. |
| `default` | string | Applied when the input is omitted (an empty string `""` is a valid sentinel). |
| `from` | string (template) | Derive the input from the trigger itself — a template rendered against `{{event.*}}` (parsed payload) and, on the url surface, `{{url.*}}` (`host`, `pathname`, `segments.N`, `query.X`). An explicit `--arg` overrides it; an empty render falls back to `default`. |

```yaml
args:
  persona:
    type: string
    required: true
  tone:
    type: string
    default: terse
  issue:
    type: string
    default: ""                      # blank → self-select
    from: "{{event.issue.number}}"   # a pasted issue URL targets this issue
instructions: |
  Draft a persona ({{args.tone}} voice): {{args.persona}}
```

Validated values substitute into `instructions` as `{{args.<name>}}`, distinct from the `{{event.*}}` payload. Validation is **strict**: an undeclared key, a missing required input, or a wrong type is rejected, and a `default` fills an omitted input. A pipeline with no `args:` accepts no inputs. Pass them via `bento trigger fire --arg key=value`, or map them off the trigger with `from:` — `{{url.*}}` lets a pipeline target off any URL shape with no daemon-side code (see [manual triggers](/triggers/manual#typed-inputs)). Targeting flags (`--branch`/`--sha`/`--pr`) stay separate and never collide with a declared input.

Trust follows the source: an operator-supplied `--arg` is trusted, but an arg filled by `from:` on a webhook/url surface may carry untrusted payload text (validation checks type, not origin), so it is wrapped in `<untrusted>` like `{{event.*}}`.

## output

Where the agent's output is sent after the run. Each key is an independent sink — set more than one (e.g. `github` and `slack`, or `diff` alongside either) and all of them fire.

| Key | Value | Description |
|-----|-------|-------------|
| `github` | `comment` | Post a comment on the triggering issue or PR. |
| `github` | `upsert_comment` | Edit the bot's prior comment on the PR if present; otherwise post a new one. |
| `github` | `review` | Post a PR review (APPROVE / REQUEST\_CHANGES / COMMENT). |
| `github` | `review_comment_reply` | Reply to the inline review comment that triggered the run. |
| `github` | `review_replies` | Reply to multiple inline review comments on the PR in one pass. |
| `slack` | `{bot, channel}` | Post to a Slack channel via the named bot (see [channels](/configuration#channels)). |
| `diff` | `{base, artifacts}` | Post nothing externally — capture `git diff <base>` from the run's worktree and write templated artifact files into the run dir. `base` is a template rendered against the event payload (e.g. `"{{payload.base_commit}}"`); each artifact has a `path` (relative to the run dir) and a templated `body` rendered against `{ payload, event, meta, workspace, diff }`. |

```yaml
output:
  github: review   # or: comment, upsert_comment, review_comment_reply, review_replies
```

```yaml
output:
  slack:
    bot: standup           # one of `channels.slack.bots` in daemon.yaml
    channel: "#standup"    # channel name, channel id, or user id for a DM
```

A pipeline's `output:` replaces `defaults.output` from `daemon.yaml` wholesale. Omit it to inherit the default; set `output: {}` to disable inherited post-back. Omitting `output` here and in `defaults` means the agent runs but its output is not posted anywhere.

## guardrails

Limits applied to the agent run. Merged per-key over `defaults.guardrails` from `daemon.yaml` — the pipeline wins where both set the same field.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `timeout` | integer (seconds) | `600` | Kill the run after this many seconds. Primary and fallback attempts share this one deadline. |
| `max_prompt_tokens` | integer (estimated tokens) | `50000` | Refuse to dispatch when the assembled prompt (system + user) exceeds this budget. Tokens are estimated as chars / 4 — an estimate, not an exact count. Prior runs shrink to fit the budget first (see `options.history.max_bytes`), so the error fires only when the remaining blocks are themselves over budget. The run fails before any sandbox is provisioned; the error itemizes per-block sizes and lands in the trigger's terminal error (`bento trigger trace`). `bento trigger inspect` warns when a matched pipeline's prompt is over budget. |
| `allowed_tools` | list of strings | — | Restrict the agent to these tools (passed to the runtime as `--allowedTools`). Omit for no restriction. |
| `read_only` | boolean | `false` | Sets `BENTO_READ_ONLY=1` in the agent's environment. Advisory only — agent prose can honor it, but the sandbox is not mounted read-only and nothing blocks writes, commits, or pushes. |

```yaml
guardrails:
  timeout: 900
  allowed_tools: [Read, Grep, Glob]
  read_only: true
```

## sandbox

Overrides the sandbox configuration for this pipeline.

| Field | Type | Description |
|-------|------|-------------|
| `backend` | `docker` | `podman` | `daytona` | Use this sandbox backend instead of the daemon default. The named backend's connection config still comes from the top-level [`sandboxes:`](/configuration#sandbox) block — `backend: daytona` here needs `sandboxes.daytona` declared. |
| `image` | string | Override the sandbox image for this pipeline's runs. Rendered as a template against `{ payload, event }`; falls back to the matched repo's [`sandbox.image`](/configuration#repos), then the daemon-wide image (or `agent:default`). Accepts the same three forms as the repo-level setting: `devcontainer`, a `./`-prefixed repo-relative path, or an image reference. Used to pin a pipeline to an instance-specific image. |
| `mounts` | list | Additional bind mounts. Each entry has `host` (path on the host), `container` (path inside the sandbox), and optional `readOnly` (boolean — mount the path read-only). |

```yaml
sandbox:
  backend: docker
  mounts:
    - host: ~/.config/gh
      container: /home/node/.config/gh
    - host: ~/.gitconfig
      container: /home/node/.gitconfig
```

Mount host credentials the agent needs at runtime.

Merged per-key over `defaults.sandbox` from `daemon.yaml` — the pipeline wins where both set the same field (`backend`, `image`, `mounts`), so a daemon-wide default `backend` combines with a pipeline-specific `image`.

## retain\_checkout

**Optional.** When `true`, the thread's `checkout/` directory — the per-thread worktree and any deps installed by `setup:` steps — is kept on disk between runs. Default: the daemon discards it when the thread's last active run ends (the checkout is regenerable and, at review volume, is the dominant disk cost). Set this for pipelines whose repeated runs amortize a warm dependency install.

Workbench threads (`workspace.checkout`) always retain regardless of this setting.

```yaml
retain_checkout: true
```

## knowledge

How knowledge base content reaches this pipeline's runs — two knobs, `method` (how the daemon retrieves) and `delivery` (how results reach the agent). See [Method & delivery](/knowledge-base/modes) for what each value does and how they compose. Merged per-key over `defaults.knowledge` from `daemon.yaml` (`filter` replaces wholesale). Omit both and runs get no knowledge injection.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `method` | `bm25` | `vector` | `none` | `bm25` | How the daemon retrieves — see the table below. `none` means no daemon-side retrieval (the agent fetches knowledge itself). |
| `delivery` | `prompt` | `file` | `browse` | `search` | `none` | `prompt` | How results reach the agent. A primary (`prompt`/`file`/`browse`) composes with `search` as `prompt+search` etc. (content for grounding, the MCP tool as escape hatch). |
| `instructions` | string | per-delivery default | Operator guidance on how to use the knowledge. When set, it renders in the user message and replaces the default system instruction. |
| `filter.categories` | list of strings | — | Only docs whose frontmatter `category` is listed. ORs within; ANDs with `tags`. |
| `filter.tags` | list of strings | — | Only docs carrying at least one listed tag. |
| `topK` | integer | `5` | Results per source (retrieving methods). |
| `maxTokens` | integer | `8000` | Injection budget (chars/4 estimate). |

| `method` | How the daemon retrieves |
|------|---------------------|
| `bm25` | Lexical BM25 retrieval over the indexed sources. |
| `vector` | Vector search. Requires embeddings (`qmd embed`); errors loudly when missing. |
| `none` | No daemon-side retrieval. |

| `delivery` | What the agent gets |
|------|---------------------|
| `prompt` | Retrieved excerpts as `<context>` blocks in the user message. Each block carries the doc's path, category, and tags as attributes. |
| `file` | With a retrieving method: the excerpts written to `.bento/retrieved-knowledge.md` in the checkout. With `method: none`: knowledge dirs bind-mounted read-only at `/knowledge/<type>` plus a generated `/knowledge/index.tsv` and `/knowledge/README.md` (docker/podman only; `filter` is a config error). |
| `browse` | The filtered doc listing; the agent reads docs on demand via MCP resources. |
| `search` | No content — a system nudge toward the `ask_knowledge` MCP tool. |
| `none` | Nothing. |

A delivery whose prerequisites are missing fails the run at dispatch rather than spawning an agent without them: `search` requires the `ask_knowledge` tool (qmd installed, knowledge sources configured), `browse` requires discovered docs, both require a local sandbox backend (the daemon's MCP endpoint is loopback-only); whole-corpus `file` (`method: none`) requires docker/podman and knowledge dirs on disk, and a `file` subset is rejected on daytona. The error names the missing capability and the fix.

```yaml
knowledge:
  method: none
  delivery: browse
  filter:
    categories: [conventions]
  instructions: |-
    Prefer recently-edited docs; cite the doc URI for anything you rely on.
```

Each retrieving run records method, delivery, query, and injected counts on the retrieve task — visible in [`bento trace`](/pipelines/traces).

## options

Per-pipeline behaviour switches.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `review_on_push` | boolean | `true` | When `false`, `pull_request.synchronize` events do not fire this pipeline. |
| `skip_reviewed_head` | boolean | `false` | When `true`, discard a dequeued PR job when the PR's live head already carries a review marker (`<!-- bento-review sha=... -->`) posted by the daemon's own login. Rapid pushes enqueue one job per push, but every run reviews the live head — once one run covers it, the remaining jobs are duplicates. Dismissed reviews don't count, so dismissing bento's review lets the next job run. Best-effort: if the GitHub API call fails, the run proceeds. |
| `skip_unchanged_head` | boolean | `false` | Scheduled pipelines only. When `true`, skip a fire when the branch head equals the head recorded by this pipeline's last completed run. The trigger is discarded before any agent spawns. Best-effort: if the head can't be fetched, the run proceeds. |
| `stateless` | boolean | `false` | When `true`, the pipeline carries no cross-run memory: prior runs and notes are neither injected into the prompt nor persisted from agent output. For recurring pipelines whose runs are unrelated (e.g. a liveness probe). |
| `state` | `true` | `{ schema }` | — | Persistent per-pipeline JSON state (BIP-22). `true` enables untyped state; `{ schema }` adds a JSON Schema surfaced to the agent in the tool description. Exposes `read_pipeline_state` / `write_pipeline_state` tools to the orchestrator — never prompt-injected automatically. Orchestrator pipelines only; local backends only (state lives on the daemon host). |
| `history.max_runs` | integer | `10` | Maximum number of prior runs injected into the prompt, newest first. The on-disk `runs/` history stays complete — only prompt injection is capped. |
| `history.max_bytes` | integer (bytes) | derived | Byte budget across the rendered prior-runs block. Older runs are dropped whole (never truncated mid-entry) until the kept runs fit. When unset, the budget is whatever remains under `guardrails.max_prompt_tokens` after every other prompt block — prior runs shrink to fit rather than tripping the prompt-budget error. Set a value to pin the budget explicitly. |

When the budget drops runs, the rendered block states how many older runs were omitted. `stateless: true` disables history entirely; `history` applies only to stateful pipelines.

```yaml
options:
  stateless: true
```

```yaml
options:
  history:
    max_runs: 20
    max_bytes: 100000
```

## setup

Commands to run after workspace setup succeeds and before the agent spawns — for anything that mutates the worktree from outside the agent (fixture loading, symbol indexing, …).

| Field | Type | Description |
|-------|------|-------------|
| `run` | string | Shell command. Runs from the daemon's cwd with workspace coordinates in the environment: `WORKTREE`, `REPO`, `SHA`, `BRANCH`, `EVENT_NAME`, `EVENT_SOURCE`, `RUN_DIR`, `SURFACE_DIR`, `PAYLOAD_JSON`. Soft-fails on non-zero exit — the agent still runs. |
| `timeout` | integer (seconds) | Hard timeout per step. Default `360`. |

```yaml
setup:
  - run: ./scripts/load-fixtures.sh
    timeout: 120
```

A pipeline's `setup:` replaces `defaults.setup` from `daemon.yaml` wholesale — the array is a single field, so a pipeline that declares its own steps does not inherit the defaults' steps. Omit `setup` here to run the daemon-wide default steps.

## workspace

Templated workspace extraction for generic [webhook](/triggers/webhook) pipelines. Each template renders against `{ payload, event }` to produce the workspace the daemon clones. Omit for `github`/`linear` pipelines — those use built-in source-aware extractors.

| Field | Description |
|-------|-------------|
| `key` | Template for the workspace key — the dedup / on-disk identifier, e.g. `"swe-bench:{{payload.instance_id}}"`. Must render to a stable non-empty string. |
| `repo` | Template for `owner/name`. |
| `branch` | Template for the branch ref. Required iff `sha` is absent. |
| `sha` | Template for the commit SHA. Required iff `branch` is absent. |

```yaml
workspace:
  key: "swe-bench:{{payload.instance_id}}"
  repo: "{{payload.repo}}"
  sha: "{{payload.base_commit}}"
```

## priority

Queue priority for this pipeline's runs, passed to Graphile Worker (lower numbers run first). Default `3`.

```yaml
priority: 1
```

## orchestrator

Explicitly enables the orchestrator agent during the spawn phase. The orchestrator is a daemon-internal LLM that decides how to dispatch the work — whether to call the named agent directly, fan out in parallel, or assemble a more complex pattern. A pipeline with no `orchestrator` field runs its named agent directly.

| Value | Description |
|-------|-------------|
| Omitted | Spawn the named agent directly. |
| `{ strategy: { type: auto }, targets?, guidance?, capabilities?, max_capability_calls? }` | Enable parent-model orchestration. Every referenced target must use the `claude` runtime. Listing `capabilities` enables typed capability mode; `max_capability_calls` defaults to `5` and must be a positive integer. |

```yaml
# Direct spawn: omit the orchestrator field.

# Enable auto orchestration and use daemon target defaults.
orchestrator:
  strategy:
    type: auto

# Override the ordered target list and provide static guidance.
orchestrator:
  targets: [claude-low]
  strategy:
    type: auto
  guidance: |
    This is a single-shot notification. No fan-out needed.
    Delegate directly to the named agent and stop.
```

The daemon-level `orchestrator` block supplies target defaults only. It does not enable orchestration for pipelines that omit the field. `guidance` is a static, non-empty string and is not rendered against `event` or `args` templates. `auto` is the only supported strategy type. Boolean values, `assess`, and unknown keys fail configuration validation.

The orchestrator runs as a direct API call in the daemon; tasks it dispatches run inside sandboxes.

### Capability mode

Capability definitions live in `.bento/capabilities/*.yaml` or `.yml`. Each file defines one operation. The registry is validated and frozen at daemon startup; restart the daemon after changing a capability definition. A pipeline-only reload can select from the registry loaded at boot.

```yaml
# .bento/capabilities/canon-review.yaml
name: canon_review
description: Review code against the engineering canon.
input:
  type: object
  properties:
    scope: { type: string, enum: [files, pull_request] }
    question: { type: string }
  required: [scope]
  additionalProperties: false
output:
  type: object
  properties:
    summary: { type: string }
    findings: { type: array, items: { type: object } }
  required: [summary, findings]
  additionalProperties: false
agent: reviewer
skill: agentic-review
targets: [claude-high, claude-backup]
access: observer
timeout: 900
```

An actor capability that performs provider actions selects one pipeline binding:

```yaml
access: actor
principal: pull-request-author
```

The selected binding must exist on every pipeline that registers the capability. Observer capabilities cannot set `principal`.

Register capabilities on the pipeline:

```yaml
orchestrator:
  strategy:
    type: auto
  guidance: Select the smallest capability that satisfies the request.
  capabilities:
    - canon_review
    - edit_pull_request
    - run_verification
  max_capability_calls: 5
```

Capability mode exposes one tool per registered capability and a capability-aware `parallel` tool. It does not expose the generic `task` tool. The orchestrator chooses gate, route, sequential, parallel aggregation, orchestrator-worker, generator/evaluator, and adaptive fan-out patterns at runtime. Calls and results are schema-validated and recorded in the existing Task trace.

Capability `targets` are ordered: the first is primary and later entries are pre-dispatch fallbacks. Omit `targets` to inherit the parent invocation route.

`observer` capabilities do not receive forge credentials, git configuration, pipeline credential mounts, or actor-only environment variables. An `actor` capability requires actor access plus an exact named grant from outside the model. For a GitHub mention pipeline, include `/allow capability_name` in the mention-bearing comment. Each grant must name a capability registered by that pipeline. Dry runs never grant actor capabilities.

Every capability must declare `access: observer` or `access: actor`; Bento does not infer an access tier.

Do not place credentials in capability inputs or outputs. Inject them through the pipeline `env:` manifest or credential mounts. Capability implementations must not echo credentials into results or free-form output. Structured fields marked `writeOnly: true` or `x-bento-sensitive: true` are redacted from Task attributes, but Bento does not scan arbitrary Task output or error text. Treat Task traces and agent logs as trusted operator data.

### Orchestrated GitHub output

Capability results do not post to GitHub. A pipeline can let the orchestrator select a presentation kind from an allow-list:

```yaml
output:
  github:
    mode: orchestrated
    allowed: [comment, review_comment_reply, review, none]
```

The final response must contain `{ "kind": "...", "body": "..." }`. Bento validates the kind and takes repository, pull-request, issue, and comment identifiers from the trigger. `review` submits a comment-only review; formal approval and request-changes events are not supported.

## context

Injects additional context into the agent's prompt at spawn time. For webhook triggers, the trigger event is always included as an `Event` section; the fields below add more.

| Field | Type | Description |
|-------|------|-------------|
| `diff` | boolean | For GitHub pull-request triggers, fetch the PR diff via `gh api` and append it as a context section. Truncated at 100 KB. |

The schema also accepts `git_history` and `related_prs` booleans; they gather nothing yet.

```yaml
context:
  diff: true
```
