Telemetry Schema
This is the authoritative wire contract between the Lowkey installer (install.sh, with the telemetry client inlined) and any telemetry backend that ingests its events. It is versioned, stable, and backwards-compatible within a major version.
Telemetry is opt-out by default via LOWKEY_TELEMETRY=0 or DO_NOT_TRACK=1. Test installs (--test mode) are flagged with is_test: true so backends can exclude them from product metrics. See Telemetry Privacy for a plain-English version.
Transport
| Aspect | Value |
|---|
| Endpoint (default) | https://telemetry.loki.run (override: LOWKEY_TELEMETRY_URL) |
| Protocol | HTTPS POST, Content-Type: application/json |
| Timeout | --connect-timeout 2s --max-time 2s |
| Delivery | Fire-and-forget background curl, detached from parent shell |
| Retry | None — all sends are best-effort |
| Auth | None required. Backend may add IP rate-limiting or a CF front door. |
| Max body | 256 KB |
| Max events per ingest batch | 500 |
Envelopes
The installer sends two envelope types on three paths:
| Path | Method | Envelope schema | When |
|---|
/v1/install | POST | lowkey.install.v1 | At install start (outcome: started), on completion (completed), on failure (failed) |
/v1/ingest | POST | lowkey.telemetry.v1 | Batched at install end — contains all queued fine-grained events |
/v1/config | GET | (response only) | Installer fetches remote kill-switch + sampling config |
lowkey.install.v1 — Install Beacon
Sent to POST /v1/install. One envelope per outcome transition (started, completed, failed). This is the primary record for install funnel analytics.
Example
{
"schema": "lowkey.install.v1",
"sent_at": "2026-04-27T12:30:14Z",
"install_id": "09ff13c6-0d4e-4e18-9d93-011e0afd9a6e",
"machine_id": "sha256:83d4cc5a36917da57120c27553c78ea83607b9b32ca23af148fc90cdca132a1f",
"agent": {
"version": "0.5.103",
"channel": "stable",
"os": "linux",
"arch": "arm64",
"os_version": "6.1"
},
"install_method": "cfn",
"outcome": "completed",
"duration_ms": 412380,
"is_test": false,
"failure_step": null,
"failure_class": null
}
Fields
| Field | Type | Required | Description | |
|---|
schema | string (const) | yes | Always lowkey.install.v1. | |
sent_at | string (ISO-8601 UTC) | yes | Wall-clock time on client, e.g. 2026-04-27T12:30:14Z. | |
install_id | string (UUIDv4) | yes | Unique per install run. Regenerated on every `curl | bash` invocation. |
machine_id | string | yes | sha256:<hex> of machine fingerprint, or fallback:<uuid> if hashing unavailable. Never the raw fingerprint. | |
agent.version | string | yes | Installer version (e.g. 0.5.103). unknown if unset. | |
agent.channel | string | yes | Release channel. Currently always stable. Reserved: beta, nightly. | |
agent.os | string enum | yes | linux, darwin, unknown. Lower-cased uname -s. | |
agent.arch | string enum | yes | arm64, x86_64, unknown. | |
agent.os_version | string | yes | Kernel major.minor, e.g. 6.1 or 23.6. | |
install_method | string enum | yes | cfn, terraform, tf, console, unknown. | |
outcome | string enum | yes | started | completed | failed. | |
duration_ms | integer | yes | ms since install start. 0 on started. | |
is_test | boolean | yes | true when installer ran with --test / TEST_MODE=true. Backends MUST exclude these from funnels. | |
failure_step | string | null | conditional | null unless outcome: failed. Short step identifier, e.g. aws_cli_check, cfn_deploy, bootstrap_timeout. Max 64 chars. | |
failure_class | string | null | conditional | null unless outcome: failed. Machine-friendly class, e.g. exit_1, exit_130. Max 64 chars. | |
Outcome lifecycle
install starts
↓ POST /v1/install { outcome: "started", duration_ms: 0 }
↓ ...user picks pack/method, downloads CFN, etc...
↓ fine-grained events recorded to local queue
↓
├─ success path ─→ POST /v1/install { outcome: "completed", duration_ms: N }
│ POST /v1/ingest { events: [...all queued...] }
│
└─ failure path ─→ POST /v1/install { outcome: "failed",
duration_ms: N,
failure_step: "cfn_deploy",
failure_class: "exit_1" }
POST /v1/ingest { events: [...all queued...] }
lowkey.telemetry.v1 — Event Batch
Sent to POST /v1/ingest. Exactly once per install run, at the end. Contains all fine-grained events that were queued locally during the run.
Example
{
"schema": "lowkey.telemetry.v1",
"sent_at": "2026-04-27T12:30:15Z",
"agent": {
"version": "0.5.103",
"channel": "stable",
"os": "linux",
"arch": "arm64",
"os_version": "6.1"
},
"machine_id": "sha256:83d4cc5a...",
"install_id": "09ff13c6-0d4e-4e18-9d93-011e0afd9a6e",
"session_id": "5e172374-29a2-442e-8f47-f15b0b8148ef",
"is_test": false,
"events": [
{ "t": "2026-04-27T12:23:35Z", "name": "install.started", "props": { "method": "cfn" } },
{ "t": "2026-04-27T12:23:42Z", "name": "install.pack_selected", "props": { "pack": "openclaw", "profile": "builder" } },
{ "t": "2026-04-27T12:23:58Z", "name": "install.method_selected", "props": { "method": "cfn", "region": "us-east-1" } },
{ "t": "2026-04-27T12:24:10Z", "name": "install.deploy_started", "props": { "method": "cfn", "region": "us-east-1", "pack": "openclaw" } },
{ "t": "2026-04-27T12:29:50Z", "name": "install.deploy_completed","props": { "duration_ms": 340000, "method": "cfn" } },
{ "t": "2026-04-27T12:30:11Z", "name": "install.bootstrap_completed","props":{ "instance_id": "i-0abc..." } },
{ "t": "2026-04-27T12:30:14Z", "name": "install.completed", "props": { "duration_ms": 412380, "pack": "openclaw", "method": "cfn", "region": "us-east-1" } }
]
}
Envelope fields
| Field | Type | Required | Description |
|---|
schema | string (const) | yes | Always lowkey.telemetry.v1. |
sent_at | string (ISO-8601 UTC) | yes | Time the flush happened. |
agent.* | object | yes | Same shape as lowkey.install.v1.agent. |
machine_id | string | yes | Same value as in install beacon for this run. |
install_id | string (UUIDv4) | yes | Same value as in install beacon for this run. |
session_id | string (UUIDv4) | yes | Regenerated every time install.sh is sourced — always equal to install_id today, reserved for future sub-sessions. |
is_test | boolean | yes | Inherited from install run. |
events | array of Event | yes | 1–500 entries. |
Event shape
| Field | Type | Required | Description |
|---|
t | string (ISO-8601 UTC) | yes | Client-side timestamp of the event. |
name | string enum | yes | Event name — see allowed events. Max 64 chars. |
props | object | yes | Flat object, primitive values only. Max 20 keys. Keys ≤40 chars, values ≤500 chars. Nested objects get JSON-stringified by the backend. |
Allowed event names
Backends MUST reject events whose name is not on this allowlist. This prevents runaway cardinality and keeps cost/dashboards predictable.
Install funnel (emitted by install.sh)
| Name | Props | Meaning |
|---|
install.started | {method} | User launched installer. |
install.pack_selected | {pack, profile} | User picked an agent pack. |
install.method_selected | {method, region} | User picked deploy method (cfn/terraform/console) + region. |
install.deploy_started | {method, region, pack} | CFN/TF apply kicked off. |
install.deploy_completed | {duration_ms, method} | Stack reached CREATE_COMPLETE or equivalent. |
install.bootstrap_completed | {instance_id} | EC2 instance finished userdata bootstrap. |
install.completed | {duration_ms, pack, method, region} | Full install success. |
install.failed | {duration_ms, exit_code, step, pack, method} | Installer exited non-zero. |
Runtime (reserved — not yet emitted by installer)
The following names are pre-registered so the agent runtime and extensions can emit them without a schema bump:
first_run, session.started, session.ended, heartbeat.daily,
command.used, feature.used, model.invoked,
error.reported, crash.reported,
update.available, update.applied, update.skipped,
auth.started, auth.completed, auth.failed,
onboarding.completed.
Event names are closed (allowlist). Event props are open — backends should tolerate unknown keys and drop values that exceed length caps.
/v1/config — Remote Kill-Switch
GET https://telemetry.loki.run/v1/config returns a JSON config that the installer may consult to throttle or disable itself. Missing/unreachable config is treated as “defaults”.
Response
{
"enabled": true,
"sample_rate": 1.0,
"flush_interval_sec": 600,
"dropped_events": [],
"latest_version": "",
"ttl_sec": 300
}
| Field | Type | Default | Description |
|---|
enabled | boolean | true | Master kill-switch. If false, installer MUST skip all sends. |
sample_rate | number | 1.0 | 0.0–1.0. Clients hash machine_id and only send if hash mod 10000 < sample_rate * 10000. |
flush_interval_sec | integer | 600 | Reserved for long-running agents. Installer flushes at exit only. |
dropped_events | array<string> | [] | Event names to drop client-side before queuing. |
latest_version | string | "" | Optional — latest known installer version. Clients may log a “you are out of date” hint. |
ttl_sec | integer | 300 | How long clients may cache this config. |
Response headers: Cache-Control: public, max-age=60.
Data-retention & privacy contract
Backends implementing this schema MUST:
| Rule | Rationale |
|---|
| Drop IP addresses before persisting. | We only care about country (from CloudFront-Viewer-Country). |
Never re-hash or de-anonymize machine_id. | Already sha256:<hex>. |
| TTL raw events ≤ 90 days. | High-volume, low-business-value. |
| TTL install beacons ≤ 365 days. | Funnel metrics are still useful long-term. |
Exclude is_test: true from all product dashboards. | Dev/CI noise. |
Reject Content-Length > 256 KB. | Abuse / accidental logs. |
| Reject batches with > 500 events. | Backpressure. |
Return 204 No Content on success. | Minimize wire overhead. |
| Never return body/stack traces on error. | Don’t leak backend details to curl pipes. |
Provide /v1/health returning 200. | Ops monitoring. |
Reference backend (DynamoDB)
A minimal compliant backend needs:
Table lowkey-install-events (beacon persistence)
- Partition key:
id (string, UUIDv4 assigned by backend)
- Sort key:
timestamp (string, ISO-8601)
- Attributes: all fields from
lowkey.install.v1 + country, source: "v1_install"
- TTL attribute:
ttl (epoch seconds, +365d)
Table lowkey-telemetry-events (per-event persistence)
- Partition key:
id (string, UUIDv4)
- Sort key:
timestamp (string, ISO-8601)
- GSI
by-name on (name, timestamp) — for per-event-name dashboards
- GSI
by-machine on (machine_id, timestamp) — for per-machine queries
- TTL attribute:
ttl (epoch seconds, +90d)
Backward-compatibility rules
schema version is in the envelope; any breaking change bumps the suffix (v1 → v2).
- Within
v1, backends MAY add new optional fields; clients MUST ignore unknown fields.
- Adding a new event name requires updating the allowlist in both the installer and backend.
- Removing a field within
v1 is forbidden.
- Renaming a field is forbidden (emit both for one major version).
Versioning
| Schema version | Introduced | Status |
|---|
lowkey.install.v1 | lowkey@0.5.103 | Current |
lowkey.telemetry.v1 | lowkey@0.5.103 | Current |