agentbox.yaml
Reference for the in-box services and tasks configuration file
agentbox.yaml lives at your workspace root and is read inside the box by the supervisor (@agentbox/ctl, shipped as agentbox-ctl). It declares tasks (one-shot setup units) and services (long-running processes) run under a DAG scheduler, plus a few host-side blocks (carry, defaults, ide).
The file is validated twice: the host CLI pre-validates it before any box work on agentbox create (a config error aborts the run), and the in-box daemon re-validates on start. Editor autocomplete and validation come free from the JSON Schema. For the conceptual guide, see services and tasks.
TIP
Point the file at the published schema on the first line so editors (e.g. the Red Hat YAML extension) give you completion and validation:
# yaml-language-server: $schema=https://agent-box.sh/schema/agentbox.schema.jsonIn-repo examples use a local relative path instead; published projects use the URL above.
Top-level keys
All top-level keys are optional, and unknown keys are rejected (additionalProperties: false). A missing or empty agentbox.yaml is completely fine — create does not fail without one.
| Key | Read by | Purpose |
|---|---|---|
services | supervisor | Long-running processes (map of name → spec). |
tasks | supervisor | One-shot units that run to completion before dependents. |
replacements | supervisor + host | Named reusable text-substitution rule-sets — see replacements. |
ide | host (agentbox code) | VS Code attach customizations; the supervisor ignores it. |
defaults | host (@agentbox/config) | Project-level AgentBox config defaults. |
carry is also declared at the top level but is host-applied and parsed separately — see its own section below. Service and task names must match [A-Za-z0-9_-]+.
A minimal valid file can be just one task — no web app or port required:
# yaml-language-server: $schema=https://agent-box.sh/schema/agentbox.schema.json
tasks:
install:
command: pnpm installServices
A service is a long-running process. It needs either command or image (not both); unknown fields are rejected.
| Field | Default | Meaning |
|---|---|---|
command | (required, or image) | Shell string (run via bash -c) or an argv array. |
image | (required, or command) | Run a docker container instead — see docker image services. |
cwd | /workspace | Working directory; relative paths resolve against /workspace. |
env | — | Extra env vars for a command service (scalars, coerced to strings). For an image service, put container env under image.env. |
autostart | true | Start automatically when the daemon boots. |
restart | on-failure | Restart policy — see needs and restart. |
backoff | — | Exponential backoff between restarts. |
needs | — | DAG dependencies. |
ready_when | — | Readiness probe — see ready_when. |
expose | — | Mark the one web service — see expose. |
ide | — | Per-service VS Code hints (host-side only). |
A service moves through pending → waiting → starting → running → ready, and can land in unhealthy, crashed, backoff, or stopped. Logs land at /var/log/agentbox/<svc>.log inside the box.
Here is the web service from examples/express-ready:
services:
web:
command: 'set -a; [ -f .env ] && . ./.env; set +a; node server.js'
needs: [install]
env:
PORT: '3000'
GREETING: 'hello from agentbox'
expose:
port: 3000
as: 80
ready_when:
port: 3000
timeout_ms: 60000
restart: on-failureManage a service from inside the box:
$ agentbox-ctl restart web
$ agentbox-ctl stop web
$ agentbox-ctl start webstop does not exit the daemon; start restarts a previously-stopped service.
TIP
Use the array form of command to avoid shell quoting; use the string form (bash -c) when you need shell features like pipes, &&, or sourcing env files.
Docker image services
For a containerized dependency (a database, cache, …) set image: instead of command: and AgentBox generates the docker start-or-run shell for you — no hand-written docker run … || docker start … block. It runs in the box's own dockerd, so a published port like 5437:5432 is reachable from other in-box services at 127.0.0.1:5437. image: is either a bare ref string (image: redis:7) or a mapping with the container config nested under it:
services:
postgres:
image:
name: postgres:17-alpine
ports: ["5437:5432"] # "<host>:<container>" (or "<port>")
env: # the container's -e env
POSTGRES_USER: optima
POSTGRES_PASSWORD: changeme
POSTGRES_DB: optima
args: "-c max_connections=200" # string OR ["-c", "max_connections=200"]; shell-tokenized
container_name: optima_db # optional; default = service name
ready_when:
port: 5437
restart: alwaysAll the usual service fields (ready_when, restart, backoff, needs, expose, autostart) still apply at the service level — only the container config (name/ports/env/args/container_name) nests under image:. The container is reused by name across box stop/start (its data lives in the per-box docker volume, which a checkpoint does not capture — see the database note in services and tasks).
HEADS UP
A change to image/ports/env reuses the existing container as-is — AgentBox never auto-docker rms it (that would wipe its data). To apply the change, docker rm <container_name> inside the box, then agentbox-ctl reload.
Tasks
A task is a one-shot unit that runs to completion. It accepts only five fields: command, cwd, env, needs, and run_once. Tasks cannot have restart, autostart, backoff, or ready_when — the schema rejects them. That is the key distinction from services.
A task moves through pending → waiting → running → done, and can land in failed or skipped. Tasks run before dependent services via needs:. Typical use: install deps, build, seed a database.
Tasks re-run on every supervisor restart (which happens on box start, not just create). So a task must be idempotent. The run_once field makes the supervisor skip an already-satisfied task for you — no more hand-rolled marker checks:
tasks:
install:
command: pnpm install --frozen-lockfile
run_once: true # skip while the command is unchanged
build:
command: pnpm build
needs: [install]run_once takes two forms:
| Form | Behavior |
|---|---|
run_once: true | The supervisor stores a marker keyed by a hash of the resolved command. A warm boot skips while the hash matches; editing the command invalidates it and re-runs. The marker lives at /var/lib/agentbox/tasks/<name> (box rootfs — captured by checkpoints, never under /workspace). |
run_once: { check: <cmd> } | Run the probe before launching; exit 0 means already satisfied (skip). No marker is written — the probe is the source of truth. Use this when the thing you'd guard on lives outside the checkpointed filesystem (e.g. a containerized database, whose data is in the in-box docker volume, not the checkpoint). The probe runs verbatim via bash -c with the box env, so use shell vars like $AGENTBOX_BOX_NAME; it does not expand {{…}} placeholders (those are render-only). |
tasks:
seed:
command: pnpm db:seed
needs: [migrate]
# Probe the DB itself — a file marker would be restored from the checkpoint
# while the containerized DB starts empty, wrongly skipping the seed.
run_once:
check: "psql -tAc \"select 1 from \\\"user\\\" limit 1\" | grep -q 1"Re-run a task in-box:
$ agentbox-ctl run-task install
$ agentbox-ctl run-task install --forcerun-task resets the task to pending so the scheduler reruns it; it is a no-op on an already-done task unless you pass --force. --force also bypasses the run_once skip (marker or check) and, for the marker form, rewrites the marker.
HEADS UP
Tasks re-run on every daemon start, not just at create. A non-idempotent task (an unguarded git init, a destructive migration) will fire repeatedly — declare run_once: (or guard it yourself). Prefer the { check } form for state that a checkpoint does not capture (containerized DB data), where a filesystem marker would desync.
ready_when
A per-service readiness probe. Exactly one of port, log_match, or http must be present.
| Field | Default | Meaning |
|---|---|---|
port | — | TCP connect probe to host:<port>. |
host | 127.0.0.1 | Host for the port probe; ignored otherwise. |
log_match | — | Regex matched against the service's output; first match flips to ready. |
http | — | HTTP(S) URL; sends GET, waits for expect_status (default: any 2xx). |
expect_status | — | Specific status to wait for; only valid with http. |
interval_ms | 500 | Poll interval (ignored for log_match). |
initial_delay_ms | 0 | Delay before the first probe. |
timeout_ms | 60000 | Total time before timing out. |
on_timeout | kill | kill re-enters the restart policy; mark_unhealthy leaves the process running but flagged. |
services:
api:
command: 'node server.js'
ready_when:
http: http://127.0.0.1:3000/health
expect_status: 200
timeout_ms: 60000The port and log_match variants are one-liners: port: 3000 or log_match: 'listening on'. Use on_timeout: mark_unhealthy as the escape hatch for legitimately slow cold starts.
TIP
Downstream units gated with needs: [api] start only once the probe reports ready, not merely when the process spawns.
expose
expose marks a service as the web service. At most one service may set it.
expose:
port: 3000
as: 80port (1–65535) is the port the service listens on inside the box. as is the container port AgentBox forwards to it. The supervisor runs an in-process TCP forwarder binding container :80 → 127.0.0.1:<port> (the box's node binary has cap_net_bind_service, so binding :80 works as non-root).
Because expose is wired by the supervisor, not the container runtime, adding or changing it and running agentbox-ctl reload activates the web service with no box restart. The setup wizard relies on this when it writes agentbox.yaml after create.
HEADS UP
as must be 80 — it is the single container port AgentBox reserves for the published web URL. Any other value is rejected.
See web apps and tunnels for how the exposed port becomes a published URL on the host. VNC and screen sharing are separate — see browser and screen.
needs and restart
needs is an array of task/service names that must reach their terminal-good state before this unit starts, forming a DAG. Independent units launch in parallel. Cycles and unknown references are rejected at config load.
restart (services only) controls relaunch:
| Value | Behavior |
|---|---|
always | Restart regardless of exit code. |
on-failure | (default) Restart only on a non-zero exit. |
never | Leave it dead after it exits. |
backoff (services only) sets exponential backoff between restarts: initial_ms (default 500), max_ms (default 30000), factor (default 2). The runtime enforces max_ms >= initial_ms — a cross-field rule the JSON Schema cannot express.
The examples/test-workspace file contrasts all three restart modes:
services:
ticker:
command: 'i=0; while true; do echo "tick $i"; i=$((i+1)); sleep 1; done'
restart: always
flaky:
command: 'echo starting; sleep 2; echo crashing; exit 1'
restart: on-failure
backoff:
initial_ms: 200
max_ms: 2000
factor: 2
one-shot:
command: 'echo "hello from one-shot"; sleep 1'
restart: neverAfter editing the DAG, apply the diff without restarting the box:
$ agentbox-ctl reload
added: (none)
removed: (none)
changed: webHEADS UP
A needs cycle or a reference to a non-existent unit fails validation — the daemon will not start, and on the host agentbox create aborts before doing any docker work.
See background and parallel for running multiple boxes, and services and tasks for the DAG model in prose.
defaults
defaults is a host-side block: project-level AgentBox config defaults, with the same shape as ~/.agentbox/config.yaml. The in-box supervisor ignores it entirely; @agentbox/config validates it strictly when the host loads it.
It sits in AgentBox's layered precedence — CLI flag > workspace > project (this block) > global ~/.agentbox/config.yaml > built-in default — so it is the place to pin per-project box-creation defaults.
defaults:
box:
provider: dockerThe full key set lives at https://agent-box.sh/schema/user-config.schema.json.
TIP
Use defaults to make a repo "remember" its provider and checkpoint so teammates get the same box without passing flags.
See configuration for the full key set and precedence. Note the cross-provider gotcha: pinning provider-native snapshot ids in shared keys can collide — the configuration page covers the per-provider box.defaultCheckpointDocker / ...Daytona / ...Hetzner / ...Vercel overrides.
carry
carry is declared at the top level but is host-applied, not parsed by the supervisor (it has its own parser). It is a declarative host→box file copy that bypasses .gitignore. Each entry maps a host src to an in-box dest:
| Field | Rule |
|---|---|
src | Must start with /, ~/, or ./. ~/ = host home; ./ = project root. |
dest | Must start with / or ~/. ~/ expands to /home/vscode. |
mode | Octal — accepts 0o600, "0600", or "600". |
user | Numeric uid that owns the file in-box (default 1000 = vscode; 0 keeps it root-owned). |
exclude | List of tar globs / bare dir names to drop when copying a directory (additive on top of the defaults below). |
optional | true skips a missing src silently instead of erroring. |
replaceEnvs | true substitutes {{AGENTBOX_*}} placeholders in the file content host-side before copying (file entries only). See replacements. |
replace | Inline replacement rules applied (in order) before copying (file only). |
rules | Names of top-level replacements: rule-sets to apply (file only). |
A shorthand string form "src=dest" (or just "src" to mirror) is also accepted.
carry:
- src: ~/.agentbox/carry-smoke/marker.txt
dest: ~/carried-marker.txt
mode: 0o600
optional: true
- src: ../legacy-app
dest: ~/legacy-app
exclude:
- "*/cache"The canonical use case is developing AgentBox inside an AgentBox: carry ~/.agentbox/secrets.env and ~/.agentbox/claude-credentials.json so the in-box CLI is already authenticated.
When copying a directory, heavy regenerable dirs (.git, node_modules, bin, obj, packages, dist, .next, target) are dropped by default; exclude: adds to that set. The host resolver blocks .. traversal, denies /proc|/sys|/dev|/etc/passwd|/etc/shadow, caps each entry at box.cpMaxBytes (default 100 MiB) after excludes — the same limit agentbox cp uses — and flags symlinks that escape $HOME and the project root. On create, claude, codex, and opencode the host prompts once — listing each src→dest with size, mode, and symlink warnings — before copying.
# auto-approve every carry entry for this box
agentbox claude --carry-yes
# skip the carry block entirely
agentbox claude --carry skipYou can also set AGENTBOX_CARRY_YES=1 or AGENTBOX_CARRY=skip. Note that -y/--yes does not auto-approve carry — a non-TTY -y with non-empty entries fails loud and asks for the explicit env var. agentbox fork is the exception: it sends carry by default (opt out with agentbox fork --carry skip).
HEADS UP
carry deliberately bypasses .gitignore and copies host files into the box. Treat the box as the untrusted side and only carry what the agent genuinely needs (credentials, env files). The per-entry cap is box.cpMaxBytes (default 100 MiB), the same limit agentbox cp enforces.
Mark credential entries optional: true so a box still comes up when a given file or agent isn't installed on the host. For how non-carry env files reach the box, see environment; for how git state is seeded, see sync and git and teleport a project.
replacements
replacements declares reusable, named text-substitution rule-sets that both the carry: block (host-side) and the in-box agentbox-ctl render CLI can reference by name. Each rule is { from, to } with optional regex / flags; to may contain {{AGENTBOX_*}} placeholders.
replacements:
box-host:
# Repoint a hard-coded hostname at this box's published URL.
- from: 'optima\.localhost'
to: '{{AGENTBOX_BOX_HOST}}' # docker/hetzner: <box-name>.localhost; vercel/daytona/e2b: the real preview host
regex: trueTwo ways to apply substitutions:
1. Carry-time (host→box files). A carry: file entry can opt into replaceEnvs: true (placeholder substitution) and/or replace: (inline rules) and/or rules: (named refs). The file is rendered host-side into a temp copy before the copy — the original host file is never modified, and the box name is known by then:
carry:
- src: ~/secrets/.env.prod
dest: /workspace/apps/saas/.env
replaceEnvs: true
rules: [box-host]2. In-box (files already in the workspace). agentbox-ctl render is a declarative sed replacement — handy for rendering a gitignored .env from a committed env.example on every boot. The render is itself idempotent (the regex rules re-pin the same lines on every boot), so this task needs no run_once: guard:
tasks:
env:
command: agentbox-ctl render apps/saas/env.example --out apps/saas/.env --env --rules box-hostPlaceholders
replaceEnvs / --env substitute a fixed whitelist of {{...}} placeholders (a stray {{FOO}} is left untouched, and secrets are never substitutable):
| Placeholder | Value |
|---|---|
{{AGENTBOX_BOX_NAME}} | The box name. |
{{AGENTBOX_BOX_HOST}} | The host this box is reachable at. Docker/Hetzner: the published portless host <box-name>.localhost. Public-URL clouds (Vercel/Daytona/E2B): the real preview host (e.g. <sub>.vercel.run), wired in at boot so it matches what agentbox url returns. |
{{AGENTBOX_BOX_ID}} | The box id. |
{{AGENTBOX_BOX_KIND}} | docker or cloud. |
{{AGENTBOX_HOST_WORKSPACE}} | Host workspace path. |
{{AGENTBOX_PROJECT_ROOT}} | Project root. |
Arbitrary substitutions go through explicit replace: / rules: rules, not the placeholder whitelist.
Generated secrets
render also expands a secret-generator token, so you can stop shelling out to openssl rand:
| Token | Behavior |
|---|---|
{{AGENTBOX_AUTO_SECRET}} | A fresh 32-byte base64url secret each render. Stable in practice because you render the template→file once (guarded). |
{{AGENTBOX_AUTO_SECRET:<name>}} | Generated once, persisted at /var/lib/agentbox/secrets/<name>, reused on every later render — stable even if you render every boot. |
# env.example
BETTER_AUTH_SECRET="{{AGENTBOX_AUTO_SECRET:better-auth}}"AGENTBOX_AUTO_SECRET is a render-only token (the persistent store lives in the box); it isn't a replaceEnvs whitelist placeholder and isn't expanded by carry:.
agentbox-ctl render <src> flags: --out <path> (or --in-place, else stdout), --env (placeholder substitution), --rules <names> (comma-separated replacements: refs), --rule 'from=>to' (literal, repeatable), --rule-regex 'pat=>repl' (regex, repeatable), and --state-dir <path> (where named secrets persist).
TIP
Reach for replaceEnvs/render instead of hand-written sed: an env task that pins BETTER_AUTH_URL/NEXT_PUBLIC_APP_URL to https://{{AGENTBOX_BOX_HOST}} becomes a one-liner, and the box and host browser then resolve the app at the same URL.
Validating the file
Editor validation is automatic via the schema modeline. The host CLI also pre-validates on agentbox create before any docker work — a config error aborts with a formatted message — and the in-box daemon re-validates on start. You can validate by hand inside the box without starting the daemon:
$ agentbox-ctl validate
OK: 1 service(s)The path argument is optional and defaults to /workspace/agentbox.yaml. The command exits with code 2 on a syntax or shape error, or a missing file.
TIP
The runtime parser and the JSON Schema are kept in lockstep by a drift test, so what your editor flags and what the daemon enforces agree — except cross-field rules like max_ms >= initial_ms, which only the runtime validator can catch.
See cli for the host-side agentbox status and agentbox logs commands that proxy into the in-box supervisor.