Schema
packages/schema/events.schema.json is the canonical wire-shape definition for events flowing into the Recording layer. The Django server loads it once at import time (apps/server/recording/views.py) and validates every ingested event against it. TypeScript clients import the matching types from packages/schema/events.ts. Both files describe the same shape and move together.
CloneEvent base fields
Every event has the same five required base fields plus an optional source_detail:
| Field | Type | Notes |
|---|---|---|
id | string (≥ 1 char) | Idempotency key. |
session_id | string (≥ 1 char) | Owning session. Auto-creates the session on first event. |
occurred_at | ISO-8601 datetime | Producer-side wall-clock time. |
source | enum | desktop | cli | mobile | smartglass | agent | integration |
source_detail | string | null | Required when source = "integration"; optional otherwise. |
type | enum | One of the nine event types below. |
Variants (the oneOf)
The schema is a oneOf over type. Each variant declares additionalProperties: false, so unknown fields are rejected.
session.started / session.stopped
No extra fields. Bookend events for a RecordingSession. The first session.started re-anchors the session's started_at, source, and source_detail; a later session.stopped sets ended_at.
app.focused
| Field | Type | Required |
|---|---|---|
app | string (≥ 1 char) | yes |
window_title | string | null | yes (null is allowed) |
capture.frame
| Field | Type | Required |
|---|---|---|
uri | string (≥ 1 char) | yes |
content_hash | string | null | yes |
width | integer (≥ 1) | yes |
height | integer (≥ 1) | yes |
input.keystroke
| Field | Type | Required |
|---|---|---|
event_type | enum (press / release) | yes |
modifiers | object (shift / ctrl / alt / meta, all booleans) | yes |
PII redaction is the producer's job — desktop today streams raw MCAP/MKV append-slabs (apps/desktop/docs/internals/upload.md) and the server contract carries a keychar/keycode strip step server-side; producers writing typed CloneEvent JSON (e.g. apps/cli) drop those fields before the wire shape leaves the device.
input.click
| Field | Type | Required |
|---|---|---|
x | integer | yes |
y | integer | yes |
button | integer (≥ 1) | yes |
modifiers | object | yes |
input.scroll
| Field | Type | Required |
|---|---|---|
x | integer | yes |
y | integer | yes |
delta_x | integer | yes |
delta_y | integer | yes |
modifiers | object | yes |
agent.prompt
| Field | Type | Required |
|---|---|---|
agent | string (≥ 1 char) | yes |
prompt | string | yes |
agent.response
| Field | Type | Required |
|---|---|---|
agent | string (≥ 1 char) | yes |
response | string | yes |
in_response_to | string | null | yes (null allowed when no prior agent.prompt is known) |
Validation behavior
- The server uses
jsonschema.Draft202012Validator. Errors are sorted by JSON pointer path; the first three are surfaced per row. - One bad row does not fail a batch. The endpoint returns
accepted,duplicates, andinvalid: [{ index, errors }]so producers can retry the failures alone. source_detailis conditionally required — the schema's top-levelallOfenforces "ifsource = "integration", thensource_detailis a non-empty string."
Examples
Valid agent.prompt
{
"id": "evt-001",
"session_id": "claude-code-2026-05-05",
"occurred_at": "2026-05-05T12:34:56Z",
"source": "agent",
"source_detail": "claude-code",
"type": "agent.prompt",
"agent": "Claude Code",
"prompt": "Test finished. What next?"
}
Valid app.focused
{
"id": "evt-002",
"session_id": "desktop-2026-05-05",
"occurred_at": "2026-05-05T12:35:00Z",
"source": "desktop",
"type": "app.focused",
"app": "Cursor",
"window_title": "apps/server/predictions/views.py"
}
Invalid — wrong source enum
{
"source": "claude-code", // ← rejected; "claude-code" is not a source enum value.
...
}
The fix is source: "agent" with source_detail: "claude-code". This is one of the most common ingest errors and was the root cause of the invalid response observed in the Quickstart before the schema rules click.
Adding a new type
Steps live in @clone/schema.