Skip to content

CLI Reference

apiary <command> [flags]

Global flags, accepted by every command:

Flag Default Description
--config apiary.yaml Config file path
--env-file .env Path to a .env file (silently skipped if not found)
--verbose off Verbose (debug) output

Daily driving

apiary run

Start the daemon: poll sources, route tasks, dispatch agents. Runs until interrupted.

apiary run [--debug] [--once] [--dry-run] [--source id] [--worker id] [--profile name]
Flag Description
--debug Verbose DEBUG logging: per-task prompt, live agent conversation, and routing decisions (view in the dashboard)
--once Poll once, dispatch all matching tasks, then exit (exit code 4 if any run failed)
--dry-run Connect to sources and match tasks, but never invoke a runner
--source Restrict to a single source id
--worker Restrict to a single worker id
--profile Activate a named runner profile from profiles.<name>

apiary dashboard

Open the terminal dashboard — a live view of tasks, agents, and logs. Run it in a second terminal, from the same directory as apiary run (they share the .apiary/ state).

apiary dashboard [--view list|board]

--view board opens on the board — tasks grouped into columns by state — instead of the list. Press b to switch either way at any time, or set settings.dashboard.default_view to make the choice stick.

apiary status

Show daemon status and active runs.

apiary status [--watch]    # --watch refreshes every 2 seconds

The status payload includes durable job counts and each registered worker's readiness, drain state, capacity, active jobs, and last heartbeat.

apiary worker

Connect a separate worker process to a control plane. The worker loads the same runner and workflow configuration but does not poll sources.

apiary worker --control-plane https://apiary.example \
  --token "$APIARY_WORKER_TOKEN" --id build-01 --pool build --capacity 4

Use repeatable --label and --capability flags to advertise scheduling attributes. On SIGTERM/SIGINT the worker enters drain mode, stops claiming, lets active jobs finish while extending their leases, then becomes unready.

apiary validate

Validate apiary.yaml — schema, reference integrity (agents → runners, steps → agents, goto targets), workflow graph rules, and condition expression syntax (if:, reject_when:, split branches, ${{ }} joins). Local subworkflow uses references are resolved recursively relative to their declaring file; typed contracts, output mappings, and reference cycles are validated before connectivity checks run.

apiary validate [--connectivity]    # --connectivity also tests each source

Inspecting work

apiary cells

List the tasks currently visible to Apiary, before routing.

apiary cells [--source id] [--unmatched] [--limit n]

--unmatched shows only items matching no trigger — useful when an issue isn't being picked up.

apiary instances

List workflow instances, or show one in step-level detail.

apiary instances [--workflow id] [--state s] [--limit n] [--json]
apiary instances <instance-id>
apiary instances <instance-id> --cancel

States: queued, running, blocked, done, failed, canceled.

A blocked instance carries a reason saying what it is waiting on — approval, ci, dependency — or interrupted when a daemon died mid-run and left it orphaned. Only the last of those is resumable; the others are alive and will continue on their own.

--cancel stops one running instance: its in-flight step is cancelled and the instance is marked interrupted, along with any queued or leased dispatch job for the same task and workflow. The source item is left alone — no labels are stripped and no state is reset — so unlike apiary restart nothing is re-dispatched, and the run can be picked up later with apiary resume <instance-id>.

Use it when a task has more than one live instance and you want to keep one of them: restart acts on the whole cell, and stopping the run from the dashboard targets the cell too.

apiary profile

Show where a run's wall clock actually went — per step, plus the slowest individual calls across the whole run:

apiary profile <instance-id> [--json]
STEP               TOTAL     THINK     WRITE     TOOLS     OTHER
plan               3m0s      67%       33%       —         —
implement          1h22m42s  6%        24%       63%       7%
  63% with background work outstanding (overlaps the above)

Slowest calls
  9m54s      background  implement       workflow:verify  ·  run the full test suite
  8m0s       tool        implement       Bash  ·  ./gradlew test

Use it before tuning a slow step: the fix for a thinking-heavy step (model or effort), a writing-heavy one (a tighter prompt) and a wait-heavy one (fix the thing being waited on) have nothing in common, and guessing wrong costs another full-length run to disprove.

Steps recorded before this data existed, and runners that stream no events, report as not measured rather than as a breakdown of zeros. See Wall-clock attribution for what each bucket covers and why the background figure overlaps the others.

--json emits the full breakdown, for the analysis this table does not do.

apiary task

Show a task's full workflow history — all instances, steps, and scoped logs. Resolve by internal task id, or by source item:

apiary task <internal-task-id>
apiary task --source github --item 1948
apiary task <id> --json

apiary improve

Analyse the execution history and propose configuration changes. Standalone — it opens the database read-only, works with the daemon stopped, and takes no dispatch slot when it is running.

apiary improve                          # analyse; print findings and a diff
apiary improve --effort deep --since 30d
apiary improve --apply                  # write the accepted changes
apiary improve --dump-evidence          # just the metrics, as JSON, no model
apiary improve --dump-prompt            # the composed prompt, no model

The evidence is computed entirely in Go, so --dump-evidence needs no advisor and costs nothing. Everything else needs an agent to reason with: --advisor, an ad-hoc --runner/--model pair, settings.improve.agent, or an agent named improver.

Past runs are recorded so their effect can be measured later:

apiary improve history
apiary improve show <run-id>
apiary improve effect <run-id>

See Self-Improvement for the evidence pack, effort levels, the validation gate and what applying does.

apiary export usage

Export per-attempt usage and cost as a file a spreadsheet can open. One row per runner attempt from task_executions, joined to the workflow instance it ran in, so spend can be pivoted by workflow, step, model, agent or ticket without querying .apiary/apiary.db by hand. Read-only, no migration, safe with the daemon running.

apiary export usage -o usage.csv                       # all history, CSV
apiary export usage --since 30d --format json          # last 30 days to stdout
apiary export usage --since 2026-09-01 --until 2026-09-02 --status failed
apiary export usage --workflow implementation --model claude-fable-5-1
apiary export usage --include-transcripts -o full.csv  # prompts and outputs too
Flag Description
--format csv (default) or json
-o, --output Write to this file; default stdout. Written to a temp file and renamed on success, so an interrupted export never leaves a truncated file
--since Window start on started_at: a duration (7d, 24h), a date (2026-09-01, midnight UTC) or RFC3339. Default: all history
--until Window end, same forms. Default: now
--workflow, --agent, --model, --source, --status Filters, each repeatable; a repeated flag means "any of". --model is an exact match. Status values: success, failed, running, pending
--include-transcripts Add input_prompt and output_text. Off by default: they dominate the file size and are not needed for cost analysis
--include-slow-tools Add slow_tools, the per-attempt slowest tool calls as a JSON string

Rows are ordered oldest first. Attempts that never started (no started_at) are excluded unless --status pending is given, in which case they come last. A database written by an older Apiary exports the columns it has and leaves the rest empty.

Columns, in order. Timestamps are RFC3339 UTC; cost_usd has six decimals; credit_exhausted is true/false; an empty cell (CSV) or null (JSON) is a value the database does not hold.

Column Meaning
execution_id Row id in task_executions
task_id, task_number, title, task_url The task at dispatch time; task_number is the human reference (ERP-42, #1948)
source_id, workflow_id, workflow_instance_id, instance_state, step_id Where the attempt ran. Empty for attempts from before the workflow engine
agent_id, runner, model, attempt, status, failure_kind, credit_exhausted Who ran it and how it ended
started_at, completed_at, duration_ms Wall clock
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, total_tokens, num_turns, num_tool_calls, cost_usd Usage as reported by the runner
time_thinking_ms, time_writing_ms, time_model_ms, time_tool_wait_ms, time_other_ms, time_background_ms Wall-clock attribution (see apiary profile)
error_message Single line in CSV
slow_tools, input_prompt, output_text Opt-in, see the flags above

Three questions the export answers in a spreadsheet:

  • Spend by workflow. Pivot cost_usd by workflow_id, then by step_id within it. The step that dominates is the one to look at with apiary improve.
  • Duplicate dispatches. Group by task_number and step_id and count rows. A ticket with the same step run many times in a short window is a retry loop or a dedup fault, and the timestamps show which.
  • One ticket's full cost. Filter task_number, sum cost_usd, and read attempt and failure_kind down the rows to see where the money went.

The column list is a contract: columns are appended, never removed or reordered, so a saved spreadsheet keeps working across upgrades.

Intervening

apiary approvals / approve / reject

Answer an approval step that has parked its workflow. See Human-in-the-loop approvals.

apiary approvals                                   # what is waiting
apiary approvals --status all --json               # every request, machine-readable
apiary approvals <request-id>                      # one request, with the fields it expects

apiary approve <request-id>                        # prompts for any declared fields
apiary approve <request-id> --field strategy=canary --field ticket=OPS-482
apiary reject  <request-id> --comment "needs a design doc first"

Values submitted with --field reach the workflow as ${{ memory.<field> }}, which is how a choice field decides what runs next. On a terminal, fields left unsupplied are prompted for; off one, a missing required field is an error rather than a prompt, so a scripted approval never hangs. A rejection never collects fields.

Exit codes: 0 resolved · 3 recorded but the gate still waits (a quorum gate) · 4 unknown or already answered · 1 transport or validation.

apiary resume

Replay a failed or interrupted workflow instance as a new immutable descendant. Cached steps are copied into the new attempt (their outputs and memory restored), then execution continues. The source attempt is never modified.

apiary resume <instance-id> [--yes]
apiary resume --workflow <workflow-id> [--yes]   # most recent failed/interrupted instance
apiary resume <instance-id> --from implement     # rerun this step and later steps
apiary resume <instance-id> --definition original # use the snapshotted definition

--definition current is the default. --definition original requires an instance created after workflow snapshots were introduced.

Compare any two attempts step by step:

apiary instances compare <before-id> <after-id>
apiary instances compare <before-id> <after-id> --json

The comparison reports state, input/output changes, token and cost deltas, and model/runner changes. Use apiary run --dry-run to evaluate source matching and routing without starting an agent.

apiary dispatch

Start one named workflow right now, whether or not anything would have triggered it. Same action as W in the dashboard.

apiary dispatch triage --item PSP-199     # run `triage` on a source item
apiary dispatch nightly-audit             # run standalone, with no source item
apiary dispatch report --input scope=q3   # standalone, with structured input

A manual run skips every gate the poll loop applies:

Skipped Meaning
the trigger's match block states, labels, types, title_regex, source — the item does not have to look like something the trigger would select
exclusive-trigger suppression a higher-priority exclusive: true trigger does not claim the task away
the live-instance guard a workflow already running on the task starts a second concurrent instance
once: true a spent one-shot workflow runs again
the consecutive-failure cap settings.max_attempts does not block the run

Every bypass is printed, so none of them is silent:

✓ Started workflow triage on PSP-199 (10042) in jira
  ! this workflow was already running on the task — a second instance is now live
  ! bypassed guard: trigger match (state/labels/filters)
  ! bypassed guard: exclusive trigger suppression
  ! bypassed guard: active instance / in-flight
  ! bypassed guard: once
  ! bypassed guard: consecutive-failure cap
  → follow it with: apiary instances

With --item the run binds a source item and behaves exactly like an automatic dispatch of that workflow: it sees the item's live labels and state, and side effects (comments, state locks, sub-issues) write back to the source. The value is the item's human reference (PSP-199, #1953) or its cell id — the same vocabulary apiary restart accepts, matched case-insensitively with a leading # ignored.

The item does not have to be one apiary is already tracking. A reference it has never polled — a ticket outside the source's filters, in an excluded state, or created since the last tick — is fetched from the source and bound on the spot, exactly as a poll would have bound it:

apiary dispatch triage --item PSP-199                 # one source: inferred
apiary dispatch triage --item PSP-199 --source jira   # several: name it

--source is optional when exactly one configured source can fetch a single item. When several can, apiary asks rather than guesses — fetching a different project's PSP-199 and running a workflow over it is not a mistake anyone catches quickly:

Error: dispatch failed: source required: 2 sources could hold it (jira, my-repo)
  — pass --source to say which

--source also disambiguates a reference that exists in more than one source, which previously required looking up the cell id by hand.

A reference no source can produce an item for still fails and creates nothing. The source that answered is named in the output and on the binding — with Jira the key you typed (PSP-199) and the cell id it resolved to (the opaque numeric issue id) are both reported, since only one of them appears in Jira's UI.

Without --item the workflow runs standalone on a fresh internal task with no source binding. Nothing writes back to a source: comment and state-lock steps are no-ops and sub-issues cannot be materialized. Pass --input key=value (repeatable) for values the steps read as ${{ input.<key> }}, and --title to name the task.

This is what makes a trigger-less workflow useful: a workflow with no trigger: block never starts on its own — apiary validate warns about exactly that — but apiary dispatch <id> runs it on demand.

Because guards are skipped rather than overridden, nothing in the daemon prevents two runs of the same workflow on the same task from racing. Where that matters — steps that mutate the same branch, comment, or item state — start the second run after the first settles.

apiary restart

Force-restart a stale task: cancel its running dispatch, cancel its queued jobs, interrupt its non-terminal instances, strip its control labels — then re-route the item and dispatch it immediately, without waiting for the next poll. Same action as R in the dashboard.

apiary restart CDT-123     # Jira key
apiary restart '#1953'     # GitHub issue number (quote it — # starts a comment)
apiary restart 1953        # the cell id also works

The argument is the item's human reference (a Jira key, a GitHub issue number) or its cell id (the raw source item id). The reference is usually what you want: on Jira the cell id is the opaque numeric issue id, which appears in no interface — the key is the only thing you can see.

Matching ignores case and a leading #. An exact cell id always wins over a number. A reference that matches items in two different sources is rejected rather than guessed at, and names both candidates so you can restart the cell id directly. An argument that resolves to nothing fails and touches nothing; if it is an internal task id, the error names the item to use instead.

Restart overrides the once and failure-cap (settings.max_attempts) guards, since a task wedged behind either is what restart exists for; any override is printed. It does not override the in-flight guard, so a live workflow is never run twice. The command reports what it dispatched:

✓ Restarted CDT-123 (10042) (control labels cleared)
  ! overrode guard: implement (failure cap)
  → dispatched 1 workflow(s): implement

The item is echoed as reference (cell id) when the two differ, so it is always clear which item was acted on. dispatched 0 means the cleanup ran but no workflow matches the item in its current state — usually a label the triggers don't match.

apiary delete

Delete a task and all its workflow instances from the database.

apiary delete <task-id> [--yes]
apiary delete --source github --item 1953 [--yes]

apiary clear

Reset the project's SQLite database (asks for confirmation; --yes skips).

Setup & service

apiary init

Scaffold a starter apiary.yaml in the current directory.

apiary plugins

Find, install and inspect out-of-process plugins. The registry half (search, info, install, upgrade, uninstall) talks to a plugin index; the rest reports on what is already on disk.

apiary plugins search [query] [--capability source]
apiary plugins info <id>[@version]
apiary plugins install <id>[@version] [--dir path] [--yes] [--sha256 digest]
apiary plugins upgrade <id> [--rollback]
apiary plugins uninstall <id> [--force]
apiary plugins list
apiary plugins inspect <id>
apiary plugins validate
Command Description
search List registry entries matching a query, optionally filtered by capability
info One listing in full: capabilities, repository, releases, what CI's conformance run found, and whether it can be installed on this host
install Resolve, verify digests, validate the manifest, print what it will place and what access it declares, then commit it atomically. Prints the plugins: snippet — it never edits apiary.yaml, and never enables anything
upgrade The same checks, then swap; keeps one generation as <id>.bak, restores it if the new copy fails to validate. --rollback restores it on demand
uninstall Remove an installed plugin directory. Refuses while it is enabled in apiary.yaml unless --force
list What is installed, with each plugin's configured state
inspect Print one installed manifest as JSON
validate Re-check installed manifests, enabled instances' config, and pinned executables

Registry commands accept --registry <url> for a one-off index and --offline to use the cached one. A plugin runs with the daemon's OS permissions; a listing is reviewed, not endorsed. See Registries and mirrors for pinning a signing key or turning the registry off.

apiary service

Manage Apiary as a system service — systemd (Linux), launchd (macOS), or Windows Service — so the daemon starts at boot and restarts on failure:

apiary service install
apiary service start
apiary service status
apiary service stop
apiary service uninstall

apiary update

Update apiary to the latest GitHub release in place:

apiary update          # download, verify the checksum, and replace the binary
apiary update --check  # only report whether a newer version exists

Downloads are validated against the release's checksums.txt before the swap. Installs managed by Homebrew or Scoop are detected and redirected to brew upgrade --cask apiary / scoop update apiary instead of self-updating. After an update, restart the daemon (apiary service stop && apiary service start) to pick up the new version.

Interactive commands also check for a new release at most once every 24 hours and print a short notice when one is available. Set APIARY_NO_UPDATE_CHECK=1 to disable the check.

apiary migrate

Apply pending database data migrations.

apiary migrate

Schema creation — tables, indices, new columns — happens automatically whenever any command opens the database, so this command is not part of a normal upgrade. The daemon runs the data migrations itself at startup; apiary migrate exists for the operator who would rather apply them deliberately, with the daemon stopped and a copy of the database taken.

Stop the daemon first — the command refuses to run while one is answering on the control socket, and tells you so. The migrations rewrite rows, and one of them recreates a table, so a concurrent writer can lose a row that lands mid-rebuild. Read-only commands (apiary dashboard, apiary memory) never run them, precisely so that opening the dashboard cannot rewrite data underneath a running hive.

Every step is idempotent — running it on an already-migrated database does nothing.

apiary version

Print the version.

Environment variables

Variable Description
GITHUB_TOKEN GitHub source polling + write fallback (see GitHub source); also raises the API rate limit for apiary update
PLANE_API_KEY Plane source API key (see Plane source)
APIARY_NO_UPDATE_CHECK Disable the daily update-check notice

Any variable referenced as ${VAR} in the config must be set in the daemon's environment or in the auto-loaded .env file.