# The customer timeline

The timeline is one person's history in the order it happened, assembled by `timeline.py` from the canonical records — conversations, calls, follow-ups, cases, demos and stage moves. It is a reading of those records rather than a log of its own, so an entry is as accurate as the record behind it and disappears if that record is removed.

- **Status:** Available
- **Audience:** both
- **In the app:** #/timeline, #/relationships
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/relationships/timeline/

## What lands on it

- **Messages and calls** — from the canonical `threads`, `messages` and `calls`, never from the provider-shaped `tenant_*` rows an adapter wrote.
- **Follow-ups** — when one was made, with its reason, and when it was completed, attempted or cancelled.
- **Stage moves** — a relationship reaching a new point in its lifecycle is an event, not just a new value.
- **Cases and onboarding steps** — the post-sale work owned by `crm.py`.
- **Demos and opportunities** — requested, scheduled, completed or cancelled, from `sales.py`.

Each of those is read from the service that owns it. There is no timeline table being written to as a side effect, which removes the whole class of faults where a screen and its underlying data disagree because one write succeeded and the other did not.

## Ordering, and the screen that deliberately does not use it

Order here is chronological, because the question a timeline answers is *what happened, and in what order*. That is worth contrasting with Needs You, which is ranked rather than chronological: the question there is *what deserves a person's attention next*, and time is only one input to that. Two screens, two orderings, on purpose.

A person's own marks live on the canonical rows — `threads.priority`, `threads.starred`, `threads.deleted_at`, and the same three on `calls`. Priority in particular is read by the engine when it picks work, so a correction a person makes on a thread changes behaviour rather than only changing a colour.

## The cost of assembling one

A timeline is the shape of query that goes wrong quietly: read a set of rows, then ask a follow-up question about each one. Home used to do exactly that — the newest 200 open threads and three more statements per thread, so 40 conversations cost 133 statements and the 200-thread ceiling meant roughly six hundred round trips. Three set-based statements made it 15, flat.

| Screen | Before | After |
|---|---|---|
| Home, 40 conversations | 133 statements, growing with the page | 15, flat |
| Triage strip, 40 rows | 83 statements | 5, flat |
| Workspace console, 40 rows | 89 statements | 11, flat |
| Owner customer list, 1,857 accounts | 5,574 statements | 3 |

> **Note** Statements are the unit on purpose. A threshold in seconds measures the machine the test happened to run on; a count of statements measures the shape of the code, and it is the same number on a laptop and in production.

## What is missing, and why

**Another workspace's history** — Isolation is enforced three times — the customer allowlist, the ORM kernel and row-level security. A person exists inside one workspace.
**Website enquiries to JBRH** — `connect_inquiries` is a control-plane table outside every workspace. A customer not seeing one is isolation working, not a missing entry.
**Provider-shaped rows** — The engine and the UI read canonical records only, which is why adding a provider changes nothing on this screen.
**An organisation-wide view** — Assembly is per person. A company gives you its people, and each of them their own timeline.

If something you expected is absent, the question is which record it would have come from and whether that record exists on *this* person — the usual answer being that it is attached to a second record for the same human. [An entry is missing from the timeline](/docs/troubleshooting/missing-timeline-entry/) works through the rest.

## Questions

### Does the timeline show replies Connect wrote but did not send?

The timeline is a record of what happened. A reply waiting for approval has not been handed to any provider and nothing has reached the customer, so it belongs to the approval queue rather than to history — [what Connect may do](/docs/autonomy/) covers where held work waits.

### Why did entries appear all at once after a merge?

Because the merged record now reads both sides. `merge_people` preserves identities, stages, follow-ups, deals, demos, cases and onboarding from both records, and the timeline assembles from all of it — the history was never lost, it was on the other row.

### Can I add a note to somebody's timeline?

Notes belong to memory rather than to history: what Connect should know about a person goes to the contact tier, where it can be read and forgotten. The timeline reports events; [person memory](/docs/relationships/person-memory/) holds standing knowledge.

## Related

- [Relationships in Connect](https://connectbyjbrh.com/docs/relationships/)
- [Customer 360](https://connectbyjbrh.com/docs/relationships/customer-360/)
- [What Connect remembers about a person](https://connectbyjbrh.com/docs/relationships/person-memory/)
- [Something is missing from the timeline](https://connectbyjbrh.com/docs/troubleshooting/missing-timeline-entry/)
- [Building the timeline, end to end](https://connectbyjbrh.com/docs/workflows/timeline-assembly/)
- [Follow-ups in Connect](https://connectbyjbrh.com/docs/follow-ups/)
- [The query cost that grows with the business](https://connectbyjbrh.com/research/n-plus-one-that-grows/)

## What this page is based on

- `docs-source/sources/CHANNELS.md` §5 — relationships, CRM and the query-cost defects
- `docs-source/sources/GENERAL.md` §3 and §5 — canonical records, triage marks and Needs You
- `docs-source/sources/GENERAL.md` §11 — measured facts
- Connect capability registry (docs-source/facts.py)
