Skip to main content

Telemetry Schema

This is the authoritative wire contract between the Lowkey installer (install.sh + lib/telemetry.sh) 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

AspectValue
Endpoint (default)https://telemetry.loki.run (override: LOWKEY_TELEMETRY_URL)
ProtocolHTTPS POST, Content-Type: application/json
Timeout--connect-timeout 2s --max-time 2s
DeliveryFire-and-forget background curl, detached from parent shell
RetryNone — all sends are best-effort
AuthNone required. Backend may add IP rate-limiting or a CF front door.
Max body256 KB
Max events per ingest batch500

Envelopes

The installer sends two envelope types on three paths:
PathMethodEnvelope schemaWhen
/v1/installPOSTlowkey.install.v1At install start (outcome: started), on completion (completed), on failure (failed)
/v1/ingestPOSTlowkey.telemetry.v1Batched at install end — contains all queued fine-grained events
/v1/configGET(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

FieldTypeRequiredDescription
schemastring (const)yesAlways lowkey.install.v1.
sent_atstring (ISO-8601 UTC)yesWall-clock time on client, e.g. 2026-04-27T12:30:14Z.
install_idstring (UUIDv4)yesUnique per install run. Regenerated on every `curlbash` invocation.
machine_idstringyessha256:<hex> of machine fingerprint, or fallback:<uuid> if hashing unavailable. Never the raw fingerprint.
agent.versionstringyesInstaller version (e.g. 0.5.103). unknown if unset.
agent.channelstringyesRelease channel. Currently always stable. Reserved: beta, nightly.
agent.osstring enumyeslinux, darwin, unknown. Lower-cased uname -s.
agent.archstring enumyesarm64, x86_64, unknown.
agent.os_versionstringyesKernel major.minor, e.g. 6.1 or 23.6.
install_methodstring enumyescfn, terraform, tf, console, unknown.
outcomestring enumyesstarted | completed | failed.
duration_msintegeryesms since install start. 0 on started.
is_testbooleanyestrue when installer ran with --test / TEST_MODE=true. Backends MUST exclude these from funnels.
failure_stepstring | nullconditionalnull unless outcome: failed. Short step identifier, e.g. aws_cli_check, cfn_deploy, bootstrap_timeout. Max 64 chars.
failure_classstring | nullconditionalnull 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

FieldTypeRequiredDescription
schemastring (const)yesAlways lowkey.telemetry.v1.
sent_atstring (ISO-8601 UTC)yesTime the flush happened.
agent.*objectyesSame shape as lowkey.install.v1.agent.
machine_idstringyesSame value as in install beacon for this run.
install_idstring (UUIDv4)yesSame value as in install beacon for this run.
session_idstring (UUIDv4)yesRegenerated per source lib/telemetry.sh — always equal to install_id today, reserved for future sub-sessions.
is_testbooleanyesInherited from install run.
eventsarray of Eventyes1–500 entries.

Event shape

FieldTypeRequiredDescription
tstring (ISO-8601 UTC)yesClient-side timestamp of the event.
namestring enumyesEvent name — see allowed events. Max 64 chars.
propsobjectyesFlat 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)

NamePropsMeaning
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
}
FieldTypeDefaultDescription
enabledbooleantrueMaster kill-switch. If false, installer MUST skip all sends.
sample_ratenumber1.00.0–1.0. Clients hash machine_id and only send if hash mod 10000 < sample_rate * 10000.
flush_interval_secinteger600Reserved for long-running agents. Installer flushes at exit only.
dropped_eventsarray<string>[]Event names to drop client-side before queuing.
latest_versionstring""Optional — latest known installer version. Clients may log a “you are out of date” hint.
ttl_secinteger300How long clients may cache this config.
Response headers: Cache-Control: public, max-age=60.

Data-retention & privacy contract

Backends implementing this schema MUST:
RuleRationale
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 (v1v2).
  • 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 versionIntroducedStatus
lowkey.install.v1lowkey@0.5.103Current
lowkey.telemetry.v1lowkey@0.5.103Current