Skip to main content

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:

FieldTypeNotes
idstring (≥ 1 char)Idempotency key.
session_idstring (≥ 1 char)Owning session. Auto-creates the session on first event.
occurred_atISO-8601 datetimeProducer-side wall-clock time.
sourceenumdesktop | cli | mobile | smartglass | agent | integration
source_detailstring | nullRequired when source = "integration"; optional otherwise.
typeenumOne 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

FieldTypeRequired
appstring (≥ 1 char)yes
window_titlestring | nullyes (null is allowed)

capture.frame

FieldTypeRequired
uristring (≥ 1 char)yes
content_hashstring | nullyes
widthinteger (≥ 1)yes
heightinteger (≥ 1)yes

input.keystroke

FieldTypeRequired
event_typeenum (press / release)yes
modifiersobject (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

FieldTypeRequired
xintegeryes
yintegeryes
buttoninteger (≥ 1)yes
modifiersobjectyes

input.scroll

FieldTypeRequired
xintegeryes
yintegeryes
delta_xintegeryes
delta_yintegeryes
modifiersobjectyes

agent.prompt

FieldTypeRequired
agentstring (≥ 1 char)yes
promptstringyes

agent.response

FieldTypeRequired
agentstring (≥ 1 char)yes
responsestringyes
in_response_tostring | nullyes (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, and invalid: [{ index, errors }] so producers can retry the failures alone.
  • source_detail is conditionally required — the schema's top-level allOf enforces "if source = "integration", then source_detail is 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.