learn · tracking plan
Event catalog
Every event, who fires it, what it carries.
In Tracking plan
The event log is the raw material for every metric and every model.
rule 1 — Append-only · A correction is a new event. Nothing is edited after it is written.
rule 2 — Ids at emit time · Every id known at that moment goes on the row. Later is too late.
rule 3 — Versions on every row · Model, prompt, rule, scorer, ICP: whatever decided.
what it is, in industry terms
An event is a fact that happened, written once and never edited.
Industry names: event tracking plan, event schema registry, append-only event log. The tracking plan says which events exist, who fires them and what each carries. The event catalog is that list, made concrete enough to code against.
Event — event · fact · Something that happened at a time, done by an actor: a draft was decided, a signal was detected, an invoice was paid.
Envelope — event envelope · The fields every event shares: id, type, time, actor, the ids it joins on, and the versions of whatever decided.
Payload — properties · The fields specific to one event type. Frozen at the time it fired.
Consumer — subscriber · What reads the log: the observer, the experiment analyzer, the daily snapshot, model training.
Schema version — contract version · Bumped when an event's payload changes shape, so old rows stay readable.
the rule this page enforces — Events are append-only. A correction is a new event (identity_unlinked, draft_decided), never an update to an old one. That is what makes the log a training set: it shows what was known at each moment.
how it works
From something happening to a row a model can learn from.
- Something happens — a reply, a click
- Emitter builds it — ids + versions
- Schema check — type, fields, version
- Append to events — never updated
- Consumers read — observer, analyzer
- Daily snapshot — counts frozen
- Training sets — labels joined
- Something happens — a reply, a click
- Emitter builds it — ids + versions
- Schema check — type, fields, version
- Append to events — never updated
- Consumers read — observer, analyzer
- Daily snapshot — counts frozen
- Training sets — labels joined
One event's path. The emitter attaches every id it has at that moment; consumers never change the row.
Fire at the moment of truth — Emit where the thing actually happens: the webhook handler for Stripe, the publish step for a post, the decision handler for a draft. Not in a nightly job that reconstructs it.
Attach every id you have — detector_id, signal_id, lead_id, campaign_id, touch_id: whatever exists at that point. Missing ids can't be added later without guessing.
Stamp the versions — model, prompt_version, rule version, scorer_version, icp_version. A row without versions can't be used to compare two versions.
Validate against the schema — Unknown type or missing required field: reject in tests, log loudly in production. Never silently drop.
Write after the business change commits — Use the same transaction, or an outbox, so an event never describes something that rolled back.
Make it idempotent — An idempotency key (for example the Stripe event id) so a retried webhook doesn't count a payment twice.
the object
One envelope for every event type.
The envelope below keeps every event in one table, with the join ids and versions lifted out of the payload into columns so they can be indexed and required.
event envelope
| Field | Type | What it holds |
|---|---|---|
event_id | uuid | Primary key. |
type | string | object.action, past tense: draft.decided. |
schema_version | integer | Version of this type's payload shape. |
occurred_at | timestamp | When it happened in the world (a post's own time, Stripe's event time). |
recorded_at | timestamp | When your system wrote it. The gap between the two is detection lag. |
actor | system | founder | lead | Who made the decision. Rule 1 of the data spine. |
user_id, account_id | uuid | Whose workspace, and which connected handle. |
ref_id | string | |
ids | object | detector_id, signal_id, person_id, lead_id, campaign_id, arm, move_id, touch_id, when known. |
versions | object | model, prompt_version, rule_version, scorer_version, icp_version. |
payload | json | Type-specific fields, frozen. |
idempotency_key | string | Dedupe key from the source (webhook event id, draft id + decision). |
{
"event_id": "6c1e…",
"type": "draft.decided",
"schema_version": 2,
"occurred_at": "2026-10-17T14:02:11Z",
"recorded_at": "2026-10-17T14:02:11Z",
"actor": "founder",
"ref_id": "draft_8f2…",
"ids": {
"lead_id": null,
"campaign_id": null,
"touch_id": null
},
"versions": {
"model": "<model id>",
"prompt_version": "proof_hook_v3"
},
"payload": {
"decision": "shipped_edited",
"slot": 2,
"variant_type": "evidence",
"edit_distance": 0.18,
"time_to_decide_s": 41
},
"idempotency_key": "draft_8f2…:decided"
}the catalog
Every event the pipeline needs.
understand · stages 1 to 4 — 5 events
brief_versioned
understand
Brief: the brief is drafted or edited
- fires when
- the brief is drafted or edited
- actor
- system · founder
- carries
- brief_version, every field with source, evidence tier and date, product stage
brief_confirmed
understand — label
Brief: the founder signs off
- fires when
- the founder signs off
- actor
- founder
- carries
- brief_version, fields corrected and how
- label
- which automatic fields were wrong
competitor_mapped
understand
Research: research adds or updates a competitor
- fires when
- research adds or updates a competitor
- actor
- system
- carries
- name, kind, channels worked, price
icp_versioned
understand
ICP: the filter is written or changed
- fires when
- the filter is written or changed
- actor
- system · founder
- carries
- icp_version, fit and not-if lines with evidence, test results
trigger_judged
understand
Triggers: a trigger is screened
- fires when
- a trigger is screened
- actor
- system · founder
- carries
- trigger, status (start, on, later, off), reason, icp_version
listen · stages 5 to 7 — 11 events
detector_versioned
listen
Detectors: a detector is created or its rule changes
- fires when
- a detector is created or its rule changes
- actor
- founder · system
- carries
- detector_id, version, trigger, source, kind, rule, strength, freshness, exclusions
detector_run_completed
listen
Detectors: a scheduled run finishes
- fires when
- a scheduled run finishes
- actor
- system
- carries
- run_id, scanned, matched, errors, duration, API cost
item_matched
listen
Signals: a raw item matches a rule
- fires when
- a raw item matches a rule
- actor
- system
- carries
- raw_item_id, url, author, posted time, full text, rule fired, score
match_judged
listen
Signals: the judge decides if it is the trigger
- fires when
- the judge decides if it is the trigger
- actor
- system
- carries
- verdict, reason, confidence, judge_model, judge_prompt_version
match_labeled
listen — label
Signals: the founder marks a match real or not
- fires when
- the founder marks a match real or not
- actor
- founder
- carries
- correct, wrong, unsure, and why
- label
- is this really the trigger
signal_created
listen
Signals: a match passes freshness, exclusions and the judge
- fires when
- a match passes freshness, exclusions and the judge
- actor
- system
- carries
- signal_id, trigger, strength, age in hours at capture
match_discarded
listen
Signals: a match fails a check
- fires when
- a match fails a check
- actor
- system
- carries
- which check failed (stale, excluded, judge no, no person)
identity_linked
listen
Leads: two records are judged one person
- fires when
- two records are judged one person
- actor
- system
- carries
- person_id, handles or domains joined, method, confidence
identity_unlinked
listen — label
Leads: the founder splits a wrong merge
- fires when
- the founder splits a wrong merge
- actor
- founder
- carries
- which link was wrong
- label
- was the merge right
lead_scored
listen
Leads: a lead's score or tier changes
- fires when
- a lead's score or tier changes
- actor
- system
- carries
- score, tier, scorer_version, frozen features
lead_reviewed
listen — label
Leads: the founder acts on a lead
- fires when
- the founder acts on a lead
- actor
- founder
- carries
- accepted, skipped, snoozed, never contact, reason
- label
- does this person fit
act · stages 8 to 10 — 7 events
campaign_started
act
Campaigns: a campaign begins
- fires when
- a campaign begins
- actor
- founder
- carries
- playbook id and version, channel, offer, segment, window, stop rule, arms
lead_assigned
act
Campaigns: a lead enters a campaign
- fires when
- a lead enters a campaign
- actor
- system
- carries
- campaign_id, arm, assignment_probability
campaign_stopped
act
Campaigns: the window ends or the stop rule fires
- fires when
- the window ends or the stop rule fires
- actor
- system · founder
- carries
- why, final counts per step
draft_generated
act
Sequence: a message or post is drafted
- fires when
- a message or post is drafted
- actor
- system
- carries
- draft id, prompt_version, randomized slot, variant type, register, full text
draft_decided
act — label
Sequence: the founder ships, edits or discards a draft
- fires when
- the founder ships, edits or discards a draft
- actor
- founder
- carries
- decision, posted text, edit distance, edit locus, seconds to decide
- label
- how good the draft was
touch_sent
act
Sequence: a message to a lead goes out
- fires when
- a message to a lead goes out
- actor
- founder
- carries
- touch_id, channel, sent time, final text
post_published
act
Sequence: a public post goes out
- fires when
- a public post goes out
- actor
- system
- carries
- post id, channel, text, scheduled vs actual time
outcomes · stage 11 — 9 events
reply_received
outcome — label
Responses: the person answers
- fires when
- the person answers
- actor
- lead
- carries
- touch_id, full text, time, class
- label
- did the message work
link_clicked
outcome — label
Responses: a tracked link is opened
- fires when
- a tracked link is opened
- actor
- lead
- carries
- touch_id, link id, time
- label
- did they act
negative_feedback
outcome — label
Responses: they say stop, block or report
- fires when
- they say stop, block or report
- actor
- lead
- carries
- which, on which touch
- label
- never repeat this
visit_started
outcome
Responses: they land on the site
- fires when
- they land on the site
- actor
- lead
- carries
- touch_id from the link, landing page, referrer
signup_created
outcome — label
Conversion: an account or waitlist entry is made
- fires when
- an account or waitlist entry is made
- actor
- lead
- carries
- account id, first and last touch, how they heard
- label
- did they sign up
activation_reached
outcome — label
Conversion: they do the first thing that predicts paying
- fires when
- they do the first thing that predicts paying
- actor
- lead
- carries
- which action, time since signup
- label
- did they activate
payment_succeeded
outcome — label
Conversion: Stripe reports a paid invoice
- fires when
- Stripe reports a paid invoice
- actor
- system
- carries
- amount, plan, customer, first or renewal
- label
- did they pay
payment_failed
outcome — label
Conversion: Stripe reports a failed payment
- fires when
- Stripe reports a failed payment
- actor
- system
- carries
- amount, attempt
- label
- warm trigger
subscription_canceled
outcome — label
Conversion: Stripe reports a cancellation
- fires when
- Stripe reports a cancellation
- actor
- system
- carries
- plan, tenure, reason
- label
- did they churn
learn · stage 12 — 4 events
snapshot_taken
learn
Loop: the daily snapshot runs
- fires when
- the daily snapshot runs
- actor
- system
- carries
- counts per rung, new signals and leads, alerts
review_completed
learn
Loop: the founder reviews the week
- fires when
- the founder reviews the week
- actor
- founder
- carries
- counts, sample sizes, constraint, hypothesis
change_shipped
learn
Loop: one thing changes
- fires when
- one thing changes
- actor
- founder
- carries
- what, where, new version, judge-by date, win rule
detector_retired
learn
Loop: a detector or playbook is switched off
- fires when
- a detector or playbook is switched off
- actor
- founder
- carries
- why, and its record
illustrative example
One waitlist join, as the events that should exist.
Here is one person, end to end. Step 5 is the one that most often goes missing.
| # | Event | Key ids |
|---|---|---|
| 1 | signal.detected | signal_id, detector (lane) |
| 2 | lead.scored | lead_id, tier Hot |
| 3 | draft.generated | draft id, prompt_version, slot |
| 4 | draft.decided | shipped_edited, edit locus hook |
| 5 | touch.sent | touch_id, lead_id |
| 6 | link.clicked | short link id, touch_id |
| 7 | visit.started | site visit, touch_id |
| 8 | signup.created (waitlist) | first and last touch |
what breaks — Step 5 is missing, so steps 6 to 8 can't be tied to step 1. The join is counted, but nobody can say which detector or draft produced it.
failure modes
How event logs go wrong, and how to catch it.
| Failure | Symptom | How to catch it | Fix |
|---|---|---|---|
| Missing join ids | sign-ups with no source | orphan rate: share of conversions with no touch chain, in the Monday report | attach ids at emit time; carry touch_id in links |
| Recomputed payloads | a model looks great offline, fails live | compare features at score time vs in the training set | freeze payloads; never backfill from current state |
| Duplicates | payments counted twice | unique index on idempotency_key | dedupe on the source's event id |
| Event before commit | events for rows that don't exist | nightly join: every ref_id resolves | same transaction, or an outbox |
| Schema drift | consumers crash or read nulls | contract tests per type | bump schema_version; keep readers for old versions |
| Clock skew | negative detection lag | alert when recorded_at < occurred_at | store both; trust the source time for occurred_at |
| Personal data in payloads | emails or DMs copied everywhere | scan payloads for patterns | store references; keep raw text in one table with a retention rule |
| Silent drops | counts dip with no cause | emit-attempt vs write counters | log and alert on rejected events |
data to capture
What this component records, and what that data trains.
record
- the envelope on every event: actor, both timestamps, every known id, every version
- idempotency key from the source
- rejected events, with the reason
label
- events that carry labels: match_labeled, lead_reviewed, draft_decided, reply_received, signup_created, payment_succeeded, subscription_canceled
trains
- every model in the tracking plan reads from here: match judge, ICP fit, reply propensity, draft quality, conversion and churn
test
- contract test per event type
- orphan rate below a set line each week
- replay a day of events into the snapshot and get the same counts