Quickstart
Five minutes from nothing to a PostgreSQL copy of your Apiary history that stays current.
Assumes pgsink is installed and you have a PostgreSQL to write to. If you do not, this will do for a look around:
docker run -d --name pgsink-target \
-e POSTGRES_USER=pgsink -e POSTGRES_PASSWORD=pgsink -e POSTGRES_DB=pgsink \
-p 5432:5432 postgres:17-alpine
1. Write a config
pgsink.yaml, next to your Apiary config:
source:
dsn: sqlite:///Users/you/.apiary/apiary.db
instance: laptop # names this Apiary in the target
wake: unix:///Users/you/.apiary/apiary.sock # optional: low-latency nudge
target:
dsn: ${env:POSTGRES_DSN}
schema: apiary
defaults:
# Whole agent transcripts. Most of the bytes, and where anything sensitive
# lives — off unless you ask for them.
exclude_columns: [input_prompt, output_text, full_output]
tables:
# Roughly 10KB of stream text per row, and Apiary prunes them anyway.
task_logs: {enabled: false}
service_logs: {enabled: false}
instance is worth a moment's thought: it identifies this Apiary in the target
and is part of every primary key, so several installations can share one
database. Changing it later means reloading.
The DSN carries a password, so keep it in the environment rather than the file.
${env:NAME} expands in source.dsn and target.dsn, and an unset variable
fails at load with the variable named.
2. Check the config against your database
This is the step that saves you time. It resolves the whole config against your real schema and reports a filter on a column your Apiary does not have, an injected field colliding with a real one, or a table name you misspelt — before anything is written.
3. Create the target tables
Additive only, and applied in one transaction.
4. Load the history
task_executions 412 rows 38ms
step_runs 908 rows 61ms
execution_events 3120 rows 102ms
…
4440 rows in 291ms
Safe to re-run: every write is an idempotent upsert.
5. Follow
It runs until interrupted. To keep it running, install the launchd agent or systemd unit.
6. Ask it something
SELECT agent_id,
count(*) AS runs,
round(sum(cost_usd)::numeric, 2) AS cost,
sum(total_tokens) AS tokens
FROM apiary.task_executions
WHERE status = 'success'
AND created_at > now() - interval '30 days'
GROUP BY agent_id
ORDER BY cost DESC;
Per-step detail lives in apiary.step_runs — tokens, cost and wall-clock
attribution per workflow step, which is usually the more interesting table:
SELECT step_id,
count(*) AS runs,
round(avg(total_tokens)) AS avg_tokens,
round(avg(time_tool_wait_ms) / 1000.0, 1) AS avg_tool_wait_s
FROM apiary.step_runs
WHERE state = 'passed'
GROUP BY step_id
ORDER BY runs DESC;
Two things to know
Cost arrives late. Apiary inserts an execution row at dispatch with zero cost and fills in the tokens and cost at completion. pgsink follows that second write — that is most of what it does — so a row you read mid-run legitimately shows zero. See the table catalog.
The sink is an archive. Apiary prunes old logs and can delete tasks; a cursor-based follower never observes a delete, so PostgreSQL keeps rows Apiary has dropped. For reporting that is usually what you want — but it is a choice, not an accident.
Next: Commands for what each one does, or Operating for metrics, health checks and the quarantine.