Business KPIs
The overview already tells you how the operator is doing — runs, spend,
grades, what it changed in itself. It cannot know how the business is doing:
revenue, signups, views, reply time live in your database or behind your APIs.
Declare the few numbers you open the dashboard for, each with a read
function, and the overview shows them as its Business band — the value in
its unit, the move against the period before, the line it travelled, and where
it stands against its goal.
Declaring them
// kpis.ts — a module of its own, so `read` may use your typed env
import { defineKpi } from '@omnislash-ai/config/public';
import { db } from './db.ts';
import { env } from './env.ts';
export const kpis = [
defineKpi({
id: 'mrr',
label: 'MRR',
unit: 'usd',
period: 'this month',
goal: 50_000,
description: 'Active subscriptions, from Stripe.',
read: async ({ now, signal }) => {
const res = await fetch(`https://api.stripe.com/...`, {
headers: { authorization: `Bearer ${env.STRIPE_KEY}` },
signal,
});
const { current, previous, daily } = await res.json();
return { value: current, previous, series: daily };
},
}),
defineKpi({
id: 'reply-time',
label: 'Reply time',
unit: 'ms',
direction: 'down',
period: 'median, 7 days',
goal: 30 * 60_000,
read: async ({ now }) => {
const { median, before } = await db.replyTimes(now);
return { value: median, previous: before };
},
}),
];
// omnislash.config.ts
import { defineProject } from '@omnislash-ai/config/public';
import { kpis } from './kpis.ts';
export default defineProject({ name: 'acme', kpis });
The config file itself still never imports env (contract.md
§ 9 and the config/env boundary); a read is brain code carried by reference,
like tools and ingest.sources, and may read env in its body. Changing the
list is a config change — it ships with your next deploy.
The declaration
| Key | What it does | |
|---|---|---|
id | required | kebab-case, unique, ≤ 48 chars |
label | required | what you call it, sentence case, ≤ 40 chars |
unit | required | how the value prints: count (1.6M), usd ($18.4k), pct — percent units, 42 prints 42% and moves in points — or ms, a duration (24m) |
direction | up | which way is better. down for churn, reply time, cost: a fall is then the good news, and the goal is a ceiling |
period | — | the span the value covers, as you would say it (last 7 days, this month); printed beside the label; previous is read as the same span just before it |
goal | — | a target in the value’s unit: a level to reach (up) or to stay under (down). The cell shows how far along it is, in words and as a bar |
description | — | what the number is and where it comes from — shown when the cell is opened |
timeoutMs | 8 000 | how long read may take (≤ 60 000); past it the read’s signal aborts and the cell says it timed out |
read | required | (ctx) => Promise<reading> — below |
At most six KPIs — the band is a glance, not a report. A seventh, a
duplicate id or a malformed declaration fails the config load with the
offending KPI named.
What read gets and returns
read({ now, timezone, signal }) → { value, previous?, series?, note? }
now— when the dashboard asked; look back from here, never fromDate.now().timezone— yourlocale.timezone, for “today” and “this week”.signal— aborted attimeoutMs; hand it tofetchand your DB driver.
| Field | |
|---|---|
value | the number now, in the declared unit. null = nothing measured yet (no traffic, no sales) — the cell says so in words instead of showing 0 |
previous | the same number over the period before; omit or null for no comparison |
series | [{ at, value }], oldest → newest, at a Date or ISO string. Drawn as the sparkline (with a readout per point); the newest 90 points are kept |
note | one short line under the number (17 of 21 slots, from Stripe), ≤ 120 chars |
The answer is checked strictly: a misspelled key (prev), a non-finite number or
a missing value is that KPI’s error, named — never a silently empty cell.
When a read fails
Each KPI is read on its own, in parallel, under its own deadline. A read
that throws, times out or answers the wrong shape shows its error in its
cell — the exception’s message, or the timeout — and every other KPI still
reads. The band’s header counts the failures; opening the cell shows the full
message and a Read again button.
The dashboard reads the list when the overview opens and every five minutes
while it stays open, plus on demand from the band’s refresh button. Your agents
read the same list through the kpi_list tool (capability runtime:read) — the
same op the band calls, so an agent and the page see the same numbers. Every read
calls your code, so keep read cheap — cache inside it if the source is
expensive or rate-limited.
Where it shows
The overview (/), under the brief and above Self-evolving: one cell per
KPI. Pressing a cell opens its detail in place — the description, the line at
full size with a per-point readout, the exact figures (now, the period before,
the change, the goal, the line’s range) and how long the read took. No KPI
declared, no band.