Metrics
A metric is a number the workspace watches: triage time, weekly signups, cost per run. Each has a name, a slug (a stable handle like triage_time that machines and zv1 address it by), an optional unit, and a series of readings — one observation each, append-only. A correction is a new reading, not an edit.
Metrics come in two kinds. A measure is a number you observe — minutes, percent, dollars; its chart is the line of readings. An event is a thing that happens — a demo booked, a ticket closed; each reading counts occurrences, and the chart shows totals per day, week, or month.
A metric can also carry standing setup: which direction is good, a standing target the chart always draws, and an expected cadence (daily, weekly, monthly). A metric with a cadence that goes quiet shows as stale — a number that stops reporting is itself a signal.
Metrics exist so expectations can bind to data. When a decision entry's expectation is bound to a metric (triage time ≤ 15 min by Aug 1), every new reading of that metric lands on the entry as evidence automatically — and the open entry shows the latest reading against the target, with a ready to settle nudge when it's met. Settlement stays yours: the reading invites it, never performs it.
The Metrics tab lists every metric with its latest reading and a watching count — how many open expectations are bound to it.
How readings get in
By hand. The Record button on any metric: value, optional as-of date, optional note about where the number came from.
Through zv1. Tell zv1 the number — "support triage is down to 12 minutes this week" — and it offers to record the reading. If the metric doesn't exist yet, zv1 can create it in the same step. Works in any tool's docked assistant, chat.zerowidth.ai, or the mobile app. Every write asks for your approval first.
From a machine. In Node, the SDK is two lines and handles batching for you. Anything else can POST directly — the API accepts readings with an API key, addressed by slug:
curl -X POST https://api.zerowidth.ai/1.0/ledger/metrics/triage_time/readings \
-H "Authorization: Bearer zw_..." \
-H "Content-Type: application/json" \
-d '{ "value": 12, "note": "nightly export", "key": "2026-08-13" }'
A write to a slug that doesn't exist yet creates the metric — no setup call first; it appears in the gallery marked auto-created until someone adopts it with a name and unit. Send "kind": "event" alongside the reading if the new metric should count occurrences rather than track a level; a metric that already exists keeps the kind it has, so a write can't change how its chart reads. The optional key makes the write idempotent: re-running the same job with the same keys returns the original readings instead of doubling the series. For bulk pushes, POST /1.0/ledger/readings/batch takes up to 500 readings across any number of metrics in one call, with the same per-item keys.
Keys are minted in accounts with per-scope permissions — the Push metric readings scope can write readings and read nothing else, which is exactly what you'd hand a cron job, a webhook adapter, or a warehouse export.
From a connected tool, on a schedule. If the number already lives in something you've connected as an MCP server, a metric can pull it in itself. On the metric page, open Sources and choose Set up a source with zv1 to start the conversation: zv1 looks at what the server returns, works out which field holds the number, shows you what it would record, and saves it as a source once you approve.
What it saves is fixed — the tool, the arguments, and the field are all decided at that moment, not re-decided each time it runs. That matters for the same reason expectations are written in advance: if the way you measure something can quietly shift, a settled expectation stops meaning much.
Each source shows what it calls, how often, when it last ran, and what it recorded. One that stops working says so on the metric, so a chart that goes quiet tells you why.
Filling in history. A new source can also fetch the past. Fill in history on any source runs the same saved instruction over a range you pick, so the chart shows what normal looked like instead of starting the day you set it up. How much arrives depends on the tool: one that reports a series fills the range, one that only answers for right now records a single point. Repeating a range is safe — readings you already have stay as they are.
Breaking a count down
An event reading can carry labels: what the count is about, such as which country a signup came from or which plan it chose. One metric then holds every country, instead of a separate metric per country.
curl -X POST https://api.zerowidth.ai/1.0/ledger/readings/batch \
-H "Authorization: Bearer zw_..." \
-H "Content-Type: application/json" \
-d '{ "readings": [
{ "metric": "new_users", "value": 42, "key": "2026-10-05", "labels": { "country": "DE", "plan": "pro" } },
{ "metric": "new_users", "value": 97, "key": "2026-10-05", "labels": { "country": "US", "plan": "pro" } }
] }'
Label names are lowercase with underscores; values are any short text. A reading can carry up to ten labels, and readings with and without labels can sit in the same metric. The key covers one set of labels, so one key per day works across every country. A write with labels to a slug that doesn't exist yet creates an event metric.
Only events take labels. A count of German signups and a count of US signups add up to all signups. Two conversion rates don't add up that way, so a measure refuses a labelled reading.
On the metric page, Break down by splits the chart by one label. The eight largest values keep their own color, the rest share Other, and readings without that label show as their own band, so the parts always add up to the whole. The table under the chart gives each value's total and share.
When you write an expectation on an event metric, Only count narrows it to the values you pick: new users from DE or AT at least 1,200 since this decision. Pick values for any of the labels, all of them, or none. Only readings that match count toward it, both now and when you settle.
An expectation on an event metric is judged on counts. "At least 1,200" means the total counted since the decision landed. "Shouldn't drop more than 5%" compares how many happened per day after it landed with how many happened per day before.
How readings get out
Everything that goes in can leave. On the metric page, Export downloads the full series as CSV or JSON. With the Read metrics scope, the API serves the same history:
# Every metric in the workspace, with its slug, name and unit
curl https://api.zerowidth.ai/1.0/ledger/metrics \
-H "Authorization: Bearer zw_..."
# Raw readings, oldest first, paginated
curl https://api.zerowidth.ai/1.0/ledger/metrics/triage_time/readings?limit=500 \
-H "Authorization: Bearer zw_..."
# Rolled up per bucket — totals for events, averages for measures
curl "https://api.zerowidth.ai/1.0/ledger/metrics/demo_booked/series?bucket=week" \
-H "Authorization: Bearer zw_..."
# An event metric's labels, with each value's total
curl https://api.zerowidth.ai/1.0/ledger/metrics/new_users/labels \
-H "Authorization: Bearer zw_..."
# Part of it (slice), or split by one label (by)
curl -G https://api.zerowidth.ai/1.0/ledger/metrics/new_users/series \
--data-urlencode 'slice={"country":["DE","AT"]}' \
--data-urlencode 'bucket=week' \
-H "Authorization: Bearer zw_..."
curl "https://api.zerowidth.ai/1.0/ledger/metrics/new_users/series?bucket=week&by=country" \
-H "Authorization: Bearer zw_..."
The readings endpoint takes the same slice, and every reading it returns carries its labels.
Wherever a reading comes from, it's the same object: one number arrives, every open expectation watching that metric hears about it.