All systems operational--:--:-- UTC

One event schema for five products

How a single envelope with an entity, an actor and a timestamp lets Analytics answer questions that cross robotics, payments and agents.

Sofia Marchetti

Product Lead, Analytics

Published
Reading time
4 min read

Most analytics products start by asking you to instrument your application. Ours started with a different problem: five modules already producing billions of events a day, each with its own idea of what an event looked like. Robotics had samples. Payments had postings. Agents had steps. Trading had executions. Joining them was a research project.

This post explains the schema we converged on, why it is deliberately small, and the kinds of questions it now answers in a few hundred milliseconds.

The envelope

Every event in ConsciousLab, from every module and from customers' own code, has the same envelope:

{
  "id": "evt_01J9ZK4R7W2Q",
  "type": "telemetry.threshold_breached",
  "module": "robotics",
  "entity": "unit/AMR-0398",
  "actor": "system/edge-agent",
  "at": "2026-10-17T13:47:52.118Z",
  "data": { "channel": "drive.motor_temp_c", "value": 62.4, "limit": 60.0 },
  "links": ["site/rotterdam-dc2", "workorder/WO-3318"]
}

Six fields are required: id, type, module, entity, actor and at. data is typed per event type and validated at ingest against a registered schema. links is optional and lists other entities the event relates to.

That is the whole contract. We spent four months arguing about it and the main outcome was removing fields.

Entities are the join key

The most important decision was that every event is about exactly one entity, written as kind/id. A robot is unit/AMR-0398. An invoice is invoice/INV-2049. An agent run is run/run_7Hq2xV. A customer is customer/cus_Halvorsen.

Because entities are first-class, Analytics can build a timeline for any entity by reading every event where it appears as the subject or in links. Open workorder/WO-3318 and you see the telemetry breach that triggered it, the agent run that created it, the purchase order the agent raised, the ledger posting that paid the supplier, and the telemetry that shows the motor running at 46 °C again afterwards.

Actors make accountability queryable

actor records who or what caused the event: user/hlindqvist, agent/support-triage, system/edge-agent, apikey/key_4410. This is the field auditors care about most, and it turned out to be the one product teams use most too. "How many refunds did agents issue without a human last week, and what was their total value?" is one filter on actor kind and one sum.

Small schema, typed data

We resisted the temptation to put common fields like amount or currency in the envelope. Different modules mean different things by them, and a field that means three things is worse than no field. Instead, data is typed per event type, and the type registry is browsable in the console. Analytics knows that ledger.posted has lines with amounts and currencies, and that order.filled has a quantity and a price, and offers the right aggregations for each.

Customers register their own event types the same way. A registered type gets validation at ingest, autocomplete in the query editor, and PII tagging per field.

Storage

Events land in a columnar store partitioned by workspace, day and module. The envelope fields are stored as dedicated columns. data is stored as typed columns per event type, created on registration, so a query that filters on data.value > 60 for telemetry reads one column, not a JSON blob.

We sustain 50,000 events per second per workspace and burst to 250,000. Across all customers that is around 3.9 billion events a day.

Questions it answers

A few queries customers ran this month, each answered in under a second over 30 days of data:

  • Median time from a robot threshold breach to the work order being closed, by site.
  • Refund value issued by agents, split by whether a human approved, against the support tickets that triggered them.
  • P&L of a trading strategy, with the AI signals that preceded each entry and their stated confidence.
  • Share of invoices paid within terms, for customers whose robots had more than three faults in the period.

None of these needed a data engineer, an export or a warehouse. They needed every module to agree on what an entity and an actor are.

Getting your own events in

Send events to the same API every module uses. If you already have an event pipeline, the envelope maps cleanly onto most of them: we have customers forwarding from Segment, Kafka and Kinesis with a few lines of transform. The events documentation covers registration, validation and the ingest API.