
# Volkom Mesh — Telemetry API Reference (for implementers)

Self-contained technical document for a developer or an AI assistant (Claude
Code, Cursor) instrumenting an agent. Two channels, **one API, one database
per tenant**. Contracts verified against the code.

## Mental model

- **Tenant** = the customer, an isolated ClickHouse database (`volkom_<slug>`).
- **Agent** = `agent_name` (a stable slug, e.g. `sales-monitor`).
- **Run** = every event sharing the same `run_id` (UUID).
  Sequence: `started` → N×`in_progress` → `completed` | `failed`.
- **Execution channel** → table `telemetry_events`. **Business channel** → table `business_metrics`.

## Authentication

Every telemetry endpoint uses the **agent's Bearer token**:

```
Authorization: Bearer <token>
```

The token is generated in the portal (**Connect your agents** → workspace key,
which works for the whole fleet; or a per-agent key from the agent's page).
A workspace token can report any `agent_name`; a per-agent key only its own
(anti-spoofing: 403 `Key bound to agent '<x>'`).

> Rate limit: 100 req/60s per token. If your fleet exceeds it, use
> `POST /telemetry/batch` (N events per request) before asking for more quota.

Base URL: `https://api.volkom.ai/api/v1`. Interactive Swagger: `https://api.volkom.ai/docs`.

---

## 1) Register the agent — `POST /api/v1/agents/register`

Idempotent (upsert). Declares description + business KPIs → populates the
catalog (`label`/`unit`/`agg`) the frontend uses as its dictionary. Status:
**200**. Requires `Authorization: Bearer`.

**Body** (`AgentRegister`):
```json
{
  "agent_name": "sales-monitor",
  "agent_version": "1.0.0",
  "description": "Monitors daily sales.",
  "telemetry_doc": "Free text describing what it reports.",
  "business_kpis": [
    {"key": "orders",  "label": "Orders",  "unit": "count",     "agg": "sum"},
    {"key": "revenue", "label": "Revenue", "unit": "money:USD", "agg": "sum"}
  ]
}
```
`business_kpis` is `[]` for agents with no business channel. Fields of each
KPI (`BusinessKPIDecl`): `key` (required), `label` (default `""`), `unit`
(default `"count"`), `agg` (default `"sum"`).

---

## 2) Execution event — `POST /api/v1/telemetry`

One POST **per event** (started, each step, close). Status: **202**.

**Body** (`TelemetryEventIn`):
```json
{
  "agent_name": "sales-monitor",
  "agent_version": "1.0.0",
  "client_id": "shopflow",
  "run_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "event": "in_progress",
  "timestamp": "2026-05-30T14:32:05Z",

  "message": "Read orders",
  "level": "info",
  "category": "Ingest",
  "source": "store/read.py:88",
  "seq": 2,
  "metrics": {"orders": 42.0},
  "links": ["https://oms.internal/batch/abc"],

  "metadata": {"region": "US"},
  "duration_ms": null,
  "records_processed": null,
  "error_type": null,
  "error_message": null,
  "output": null,
  "workflow_run_id": null
}
```

**Fields:**
| Field | Type | Notes |
|-------|------|-------|
| `agent_name` | str req | |
| `agent_version` | str req | |
| `client_id` | str req | = tenant slug |
| `run_id` | UUID req | **the same for every event of the run** |
| `event` | enum req | `started` \| `in_progress` \| `completed` \| `failed` |
| `timestamp` | ISO-8601 req | UTC recommended |
| `message` | str | human description (key in `in_progress`) |
| `level` | str | `debug`\|`info`\|`warning`\|`error`\|`success` (default `info`) |
| `category` | str | grouping/phase; the frontend groups steps by it |
| `source` | str | `file:line` |
| `seq` | int | order within the run (default 0) |
| `metrics` | map<str,float> | numeric, chartable measurements |
| `links` | list<str> | external URLs/refs |
| `metadata` | map<str,str> | free flat attributes |
| `duration_ms` | int? | usually on close |
| `records_processed` | int? | usually on close |
| `error_type` / `error_message` | str? | on `failed` |
| `output` | obj? | rich nested payload |
| `workflow_run_id` | str? | if the run is part of a Temporal workflow |

**Pattern of a run (3+ POSTs with the same `run_id`):**
```
POST /telemetry  {event:"started",     run_id:R, timestamp:t0}
POST /telemetry  {event:"in_progress", run_id:R, seq:1, message:"…", category:"…", metrics:{…}}
POST /telemetry  {event:"in_progress", run_id:R, seq:2, message:"…", level:"warning"}
POST /telemetry  {event:"completed",   run_id:R, duration_ms:16000, records_processed:42}
```

---

## 3) Business KPIs — `POST /api/v1/telemetry/business`

One POST per run (that run's KPIs). Status: **202**. Separate channel;
creates/extends columns on demand in `business_metrics`.

**Body** (`BusinessTelemetryIn`):
```json
{
  "agent_name": "sales-monitor",
  "agent_version": "1.0.0",
  "client_id": "shopflow",
  "run_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "timestamp": "2026-05-30T14:32:20Z",
  "metrics": {"orders": 42, "revenue": 9500},
  "workflow_run_id": null
}
```
`metrics`: map of `key` (must match a KPI declared at register) → value
(number|string). `run_id` can be the same as the execution run to correlate
them. `agent_version`/`client_id` are optional but recommended.

---

## Via the carcass (volkom-core) — equivalent, no HTTP

If the agent is Python, subclassing `BaseAgent` does the 3 POSTs for you and
handles `run_id`/`started`/close/timings automatically.

```python
from volkom_core import BaseAgent, AgentResult, KPI, VolkomConfig, TelemetryExporter

class SalesMonitor(BaseAgent):
    agent_name = "sales-monitor"
    agent_version = "1.0.0"
    description = "Monitors daily sales."
    telemetry_doc = "Orders and revenue per run."
    business_kpis = [
        KPI("orders",  "Orders",  "count",     "sum"),
        KPI("revenue", "Revenue", "money:USD", "sum"),
    ]

    def run(self) -> AgentResult:
        self.event("Connected to the store", category="Ingest", source="store/conn.py:20")
        self.event("Read orders", category="Ingest", metrics={"orders": 42})
        self.warn("3 orders without address", category="Validation", metrics={"incomplete": 3})
        self.event("Loaded into the dashboard", category="Close",
                   links=["https://oms.internal/batch/abc"])
        self.kpi("orders", 42)
        self.kpi("revenue", 9500)
        return AgentResult(records_processed=42)
```

**Carcass API:**
- `event(message, *, level, category, attributes, metrics, links, source, kind)`
  — emits an `in_progress` (or `start`/`finish` via `kind`). `attributes` → `metadata`.
- `step / info / warn / error (message, ...)` — per-level wrappers.
- `kpi(key, value)` — business KPI (must be in `business_kpis`).
- `emit_metric(key, value)` — free metadata on the closing event (NOT a KPI).
- `started`/`completed`/`failed`, `run_id`, `duration_ms` → automatic in `execute()`.

**Registration (once, when the client starts):**
```python
exporter = TelemetryExporter(cfg)  # cfg = VolkomConfig(client_id, control_plane_url, auth_token)
exporter.register_agent(
    agent_name=SalesMonitor.agent_name,
    agent_version=SalesMonitor.agent_version,
    description=SalesMonitor.description,
    telemetry_doc=SalesMonitor.telemetry_doc,
    business_kpis=[{"key": k.key, "label": k.label, "unit": k.unit, "agg": k.agg}
                  for k in SalesMonitor.business_kpis],
)
# run once (generates run_id + started + close):
SalesMonitor(cfg, exporter).execute(run_id=<uuid|None>, workflow_run_id=<str|None>)
```

---

## KPI vocabulary

**`unit`:** `count` · `money:<ISO>` (e.g. `money:USD`, `money:ARS`, `money:EUR`) ·
`percent` · `duration` (value in seconds).

**`agg`** (how it totals over the dashboard period):
`sum` (cumulative: sales, invoices) · `last` (states: stock, MRR, % occupancy) ·
`avg` (averages: wait time) · `max` (peaks).

> Picking the wrong `agg` produces wrong totals: `sum` of a state inflates the
> number; `last` of something cumulative understates it.

---

## Reading (what the frontends consume — reference)

Same API, same DB; the portal and the admin panel read from here:
- `GET /api/v1/tenants/{slug}/dashboard/runs?include_steps=<bool>&since=&until=&limit=`
  — runs; with `include_steps=true` it includes the `in_progress` events.
- `GET /api/v1/tenants/{slug}/runs/{run_id}/events` — full narrative of one run.
- `GET /api/v1/tenants/{slug}/business/catalog` — declared KPIs (label/unit/agg).
- `GET /api/v1/tenants/{slug}/business/summary?time_range=` — KPIs aggregated by `agg`.
- `GET /api/v1/tenants/{slug}/business/series?agent_name=&key=&time_range=&bucket=` — trend.

---

## Implementation checklist

1. [ ] `POST /agents/register` with declared `business_kpis` (idempotent).
2. [ ] Per run: one unique UUID `run_id`, **reused across all its events**.
3. [ ] `started` at the beginning; `in_progress` for each step (with `category`,
       `metrics`, `level` as appropriate); `completed`/`failed` at the end with `duration_ms`.
4. [ ] Business KPIs via `POST /telemetry/business` (keys = the declared ones).
5. [ ] Header `Authorization: Bearer <token>` on everything.
6. [ ] ALWAYS close the run (no close → perpetual "in progress").
7. [ ] Errors with `level:"error"` / `event:"failed"` + `error_type`/`error_message`.

## Errors and gotchas

- **Different `run_id` per event** → every step shows as a separate run. Reuse it.
- **Run without a close** → shows as "in progress" forever.
- **Undeclared KPI** → the value gets in but without catalog metadata (unit/agg).
- **Rate limit 429** → respect 100 req/60s per token; for bursts, `POST /telemetry/batch`.
- **Anti-spoofing 403** → a per-agent key can only report its own `agent_name`.
