CLI Reference
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.
| 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).
--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.
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.
Inspecting work
apiary cells
List the tasks currently visible to Apiary, before routing.
--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:
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 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:
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_usdbyworkflow_id, then bystep_idwithin it. The step that dominates is the one to look at withapiary improve. - Duplicate dispatches. Group by
task_numberandstep_idand 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, sumcost_usd, and readattemptandfailure_kinddown 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 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.
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.