*_history table in the same transaction, and every read can be pinned to a past moment — without the caller needing to know anything about partitions, GIST indexes, or RLS policies.
There are two independent time axes:
- System time (
as_of) — when did we learn the fact? This is what moved when a write happened. - Valid time (
valid_at) — when was the fact true in the world? Useful when a correction backfills an event: the write (system time) happens now, but the fact was true yesterday (valid time).
valid_at = as_of — a single axis. Set both when you specifically want to decouple them.
What’s tracked
Four wisdom entities carry full bitemporal history: contacts, deals, notes, tasks. Each exposes the same read surface. Tasks are unitemporal — only valid time, no system time. Theiras_of flows into the valid-time axis; valid_at is format-validated but ignored for data narrowing. Task responses carry only valid_from / valid_to in the _temporal block.
Single-moment reads
Add?as_of= (RFC 3339) to any wisdom-entity GET or list endpoint.
_temporal block:
_temporal or surface it for audit UX.
List-endpoint caveats: filters that join non-history tables (full-text search q, tag / label names, stage-type, priority-key sort) return 400 when combined with as_of. The historical list otherwise mirrors the current-state one.
Full version history
/deals/{id}/version-history for deals (the plain /history route there is legacy stage-change history and returns a different shape). Contacts, notes, and tasks use /history.
Cursors are opaque. Don’t parse them; always round-trip the
next_cursor value unchanged.Diff between two moments
intermediate_versions counts every history row in the window — use it to decide whether to follow up with /timeline for the play-by-play.
Edge cases:
- Entity existed at
frombut not atto→"deleted_in_window": true. - Entity didn’t exist at
frombut did atto→"created_in_window": true. - Entity didn’t exist at either →
404.
Timeline
fields parameter is a comma-separated filter — omit it to get every field.
Retention
Each tenant has a retention window (Free 30d, Pro 90d, Scale 365d, Enterprise forever, overridable ontenant_configs). Reads beyond the retention cutoff return 410 Gone rather than empty data so callers can tell the difference between “no such entity at that moment” and “that moment is no longer available.”
MCP
Thecontext_for_person composite MCP tool accepts as_of and valid_at and returns the contact block as of that moment. Related-entity fan-out (interactions / tasks / notes / relationships) stays current-state at v0.1.
Full composite temporal tooling (trajectory, cohort_drift, relationship_signals, etc.) is in a follow-up release.
What’s not covered (v0.1 gaps)
- Related-entity fan-out on as-of reads. Historical channels, tags, labels, deal_contacts, and backlinks read the current-state tables; only the main row is historical.
- Historical full-text search.
search_vectoris generated on the main table only, soqwithas_ofreturns400. - Historical pipeline / stage / project names. These joins stay current-state. If a pipeline was renamed, as-of deal responses show the current name.
docs/tasks/temporal-rls-and-pool.md §3.3.