The event log

Every action the placement team takes, written as a row, as a side effect of them doing their normal work. One append-only stream. No new clicks, no message text, no checklists.

The shape of a row

Six fields, every time

Each row is a fact in the past tense. Nothing is interpreted at write time — status, gaps, dashboards and rubrics are all read out of the log later, never written into it.

WhenTimestamp
WhoActing agent, from their sign-in
ClientWhich client it concerns
WhatEvent type, from a closed list
SourceAgent action · system · inbound
DetailThe facts of that action, plus provider IDs
The one rule for comms. The log records that a message was sent and the provider's ID for it — never the words. Twilio and Gmail already hold the messages; the log points at them. Nothing the client says is copied anywhere.
Reading the status column

What already exists, and what doesn't

Roughly a third of this is already being recorded somewhere — it's just scattered across eight separate tables that don't talk to each other. The rest is new.

Live

Already captured today. Needs moving or copying into the stream, not inventing.

Partial

The underlying thing is recorded, but not as an event you can read in sequence — it's buried in a booking record or a provider's history.

To build

Nothing is recorded at all. The action fires and vanishes.

Elsewhere

Another system already logs this properly. The job is joining it up, not rebuilding it.

The catalogue

Every event, and what its row carries

Thirty-five event types across ten areas. This is the thing to argue with — add, cut, or tell me a row is carrying the wrong facts.

Event Fires when What the row carries Status
The call
Call started The agent dials out, or an inbound call rings. call id direction number dialled PartialOnly one row is written today, at hang-up — there's no record of the attempt itself.
Call ended Either side hangs up. call id connected y/n seconds Live
Outcome recorded The agent marks the booking after the call. outcome booking id PartialThe only outcomes that exist are “completed” and “no-show”. Everything else is invisible.
Wrap card saved The agent taps through the post-call card. the tapped options call id Live
Transcription started The client consents and the agent starts captions. call id consent given — the fact only, never a word of it To build
Call assessment written The AI reviewer scores the agent at hang-up. assessment id call id Live
Matching
Matcher run Every single search, not just the last one. The trail is the evidence. filters used what the form said what changed result count top therapists + scores To buildNothing about searching is recorded today. This is the row that derives “couldn't find anyone”.
Therapist profile opened The agent opens a card to read it properly. therapist id their score position in results To buildOpen question — is this useful evidence of the work, or noise? Your call.
The recommendation
Recommendation sent The agent sends the client their shortlist. link token therapists in order score vs the form score vs the call full match breakdown channel To buildRecords absolutely nothing today. The scores must be snapshotted — recomputing later gives a different answer.
Money
Payment link sent The agent sends the checkout link. therapist id price link slug channel message id To buildRecords nothing today.
Payment link clicked The client opens it for the first time. link slug Live
Payment received The order completes. order id amount therapist paid for Live
Sessions — with the therapist
Session booked The client is booked in with their therapist — usually by the agent right after they pay on the call. booking id therapist date & time booked by agent or self-serve PartialWritten into the booking record, not readable as a sequence — and the agent-booked vs self-serve route isn't distinguished at all.
Session cancelled or moved A booking changes after the fact. booking id old & new time cancelled by client, agent or therapist Partial
Refund requested A cancellation is escalated for a refund. order id who asked reason To buildRefunds are gated, so the request and the decision are two separate facts worth keeping apart.
Refund issued The money goes back. order id amount who approved reason the booking it relates to To buildThe link between the cancellation, the refund and the original session is what makes “why did this happen” answerable.
Calls — with the placement team
Follow-up call booked A second call with the placement team is scheduled. booking id which agent date & time booked by agent or self-serve Partial
Follow-up call cancelled or moved That call changes. booking id old & new time who did it Partial
Automatic emails — sent by the system, not by a person
Automatic email to client Session completed, reorder prompt, renewal heads-up, and the rest. which email session id order id sent or failed ElsewhereSession Accounting already logs every one of these — attempt, sent and failed — in its own audit log.
Automatic email to therapist A client is placed with them, a block completes, a payment lands. which email therapist session id sent or failed Elsewhere
Automatic email failed A send doesn't go out. which email recipient why ElsewhereThe most valuable row on this whole page and nobody currently sees it on a client's timeline.
Communication — the action and the pointer, never the words
Text sent The agent sends an SMS from the cockpit. Twilio message id which number BlockedThe send returns only true/false — the message ID is thrown away. Has to be fixed at the send before there's anything to point at.
Text received A client texts in. Twilio message id which number — no content Partial
Email sent The agent sends from the composer. Gmail message id thread id which address To buildBoth IDs already come back from the send — they're just not kept.
Email received A client replies. Gmail message id thread id — no content Partial
Voicemail received A missed call leaves a message. recording id seconds caller Partial
Therapist messaged The agent writes to the client's therapist. Gmail thread id therapist id To build
Meeting link sent The agent sends the client their session link. booking id channel message id To buildDepends on the Copy button coming out — see below.
Login link sent The agent gets the client into their account. auto-login or reset channel message id To buildCopy-only today — a send button has to exist before the copy can go.
Note added The agent writes a note on the client. note id Live
Intent — what we said we'd do next
Chase date set The agent picks a date to check back in. the date Live
Chase date cleared The date is removed without closing. Live
Chase closed The agent stops chasing for good. reason Live
Identity
Alias added or removed A second email or number is tied to a client. the identity added or removed Live
Therapist thread assigned A therapist conversation is tied to a client. thread id client reassigned from Live
Where it lives

Straight into the warehouse

The cockpit and DataHub share a VPS, so the log is written directly into DataHub's Postgres — not into WordPress, and not pulled from anywhere.

It's a table in ops. That's already where DataHub keeps the tables its own app writes — identity joins, households, pinned items, planner scenarios, team notes. None of those are pulled from WordPress either. The cockpit log is another one of them, written over localhost the moment an agent does something.
Three things to settle.
1. A failed log write must never break the thing it's recording. If Postgres hiccups the text still sends and the booking still happens. Logging is fire-and-forget, always outside the path.
2. The table definition lives in DataHub's schema directory with the rest, applied by the same script — otherwise two repos both think they own it.
3. Two identity systems now feed one timeline: the cockpit resolves a client to their aliases, DataHub does its own joining. Write whichever address the action actually used and let DataHub collapse them, the same as every other source. Don't resolve it at write time.
What Session Accounting's audit still needs. It's keyed by session, not by client — so every automatic email has to join through the session to land on the right person's timeline. And the timeline has no “who did it” column today; if you want to filter it by agent, that has to be a real field rather than something buried in the detail.
Deliberately absent

What the log will never hold

Worth stating plainly, because every one of these is a thing someone will eventually suggest adding.

The prerequisite

Copy buttons out, send buttons in

A copied link leaves no trace — the agent pastes it wherever and the log goes blind. Three places have one today.

  • The session card. Already has “Text it”. Copy can go straight away.
  • The pay-link drawer. Already has a send path. Copy can go.
  • The account access card. Copy-only — a send action has to be built first, or the agent is left holding a link they can't deliver.
Why this is the first job. Four of the events above only ever exist if the link goes out through the cockpit. Remove the copy buttons and they log themselves. Leave them and the log has holes exactly where the important actions are.
What reads it later

Everything else is downstream

None of this is being built now. It's here so you can check the log carries enough to make it possible.

Two things still open. Whether “therapist profile opened” is signal or noise. And whether the identity events belong in this stream at all — they have no client attached, so they can't sit on a client timeline, which is an argument for keeping them in a separate admin audit.