# Threadline domain reference
This guide defines the vocabulary Threadline uses across capture triggers, Ecto schemas, and the public API. It complements the [README](../README.md) and module documentation on [HexDocs](https://hexdocs.pm/threadline).
## Ubiquitous language
| Term | One sentence | Tier |
|------|----------------|------|
| AuditAction | A semantic “who did what and why” event your application records explicitly. | persisted row |
| AuditTransaction | A database transaction bucket produced by capture, grouping row-level changes and optional actor context. | persisted row |
| AuditChange | One captured INSERT/UPDATE/DELETE on an audited table, tied to a transaction. | persisted row |
| AuditContext | Request-scoped metadata (actor, request/correlation IDs, IP) carried on the connection before it reaches the database. | concept only |
| ActorRef | Structured identifier for who performed an action or triggered writes, stored as JSON-compatible data. | field on row |
| Correlation | Cross-cutting identifier linking work across processes or services (headers, job args), not a first-class DB entity in Threadline. | concept only |
## Relationships
```text
AuditAction AuditTransaction
| |
| optional link |
+--------------------------------+
| |
| v
| AuditChange
| (one row op)
v
(semantic intent) (physical capture)
```
Invariant: every `AuditChange` belongs to exactly one `AuditTransaction`; an `AuditTransaction` may link to zero or one `AuditAction` when you correlate semantic intent with physical changes.
## AuditTransaction
An `AuditTransaction` is the capture substrate’s grouping record for a single database transaction. PostgreSQL assigns `txid`; Threadline stores it with `occurred_at`, optional `source`/`meta`, and optional `actor_ref` populated from a transaction-local GUC set in the same database transaction as your writes. It may reference an `AuditAction` when you connect semantic events to captured rows.
## AuditChange
An `AuditChange` is one row-level mutation on an audited table: schema/name, primary key map, operation (`op`), optional `data_after`, changed field list, and `captured_at`. Multiple changes in one DB transaction share the same `transaction_id`.
## Redaction at capture
Threadline can **exclude** or **mask** configured columns when PL/pgSQL capture functions are generated (`mix threadline.gen.triggers`), so JSON written to `audit_changes` never contains raw values for those keys. **`exclude`** removes keys from `data_after` (and from change lists where the generator applies the same filter). **`mask`** keeps the key but persists only a stable placeholder (default `"[REDACTED]"`) for both `data_after` and sparse **`changed_from`** when that mode is enabled. Overlap between exclude and mask is a hard error at codegen. **json/jsonb** columns use whole-value masking only. Configuration lives under **`config :threadline, :trigger_capture`** (see README). Path B is preserved: redaction is static SQL and trigger paths do not introduce new session writes.
## Retention (Phase 13)
Operators cap table growth with a **global retention window** under **`config :threadline, :retention`**, validated by `Threadline.Retention.Policy` before purge runs.
- **Primary clock:** eligibility uses each row’s **`AuditChange.captured_at`** (`timestamptz`, microsecond precision), not `AuditTransaction.occurred_at`. This matches `Threadline.Query.timeline/2`, which applies **`captured_at >= :from`** (inclusive lower bound) and **`captured_at <= :to`** (inclusive upper bound) when those filters are set. Retention purge deletes changes with **`captured_at` strictly less than** the computed cutoff (`now` minus the configured window), so the boundary is **exclusive on the “keep” side** at the cutoff instant — slightly stricter than the inclusive `:to` filter in timeline queries; operators should treat the cutoff as “anything older than this instant is eligible.”
- **Global window:** v1.3 is one documented interval for all captured changes (same relation Threadline owns). Per-table retention is an extension point for later releases.
- **Long transactions:** multiple `AuditChange` rows under one `audit_transactions` row can carry **different** `captured_at` values; retention is evaluated **per change row**, not “whole transaction expires as one timestamp.”
- **Empty parents:** after eligible changes are removed, the default purge path deletes **`audit_transactions`** rows that have **no** remaining child changes (optional `delete_empty_transactions: false` for transitional installs). See `Threadline.Retention` / `mix threadline.retention.purge`.
## Export (Phase 14)
Read-only exports for operator playbooks (“export then purge”, cross-checks, ad-hoc analysis).
- **Filter vocabulary:** identical to `Threadline.Query.timeline/2` — `:repo`, `:table`, `:actor_ref`, `:from`, `:to`, `:correlation_id`. Bounds apply to **`AuditChange.captured_at`** (inclusive). **`AuditTransaction.occurred_at`** appears inside exported transaction context and can differ from `captured_at` on the same change row.
- **APIs:** `Threadline.Export` (`to_csv_iodata/2`, `to_json_document/2`, `count_matching/2`, `stream_changes/2`), `Threadline.export_csv/2`, `Threadline.export_json/2`, and **`mix threadline.export`** (see task `@moduledoc`).
- **Formats:** CSV uses a fixed hybrid column layout (JSON blobs for nested maps, single `transaction_json` column). JSON uses **`format_version: 1`** on the wrapped document; **`ndjson`** omits the outer wrapper.
- **Safety:** default **`max_rows`** caps in-memory materialization; results report **`truncated`** when the cap is hit. Streaming ignores that cap — compose with `Stream.take/2` when needed.
## Audit indexing (integrator-owned)
Physical PostgreSQL indexes on **`audit_transactions`**, **`audit_changes`**, and **`audit_actions`** are **integrator-owned**: Threadline ships a safe baseline via migrations, but workload-specific btree/GIN choices stay with the team operating the database. For baseline inventory, join shapes (timeline vs export vs correlation), retention delete patterns, and **optional** additive DDL framed as non-mandatory, read the **[Audit table indexing cookbook](audit-indexing.md)**—do not duplicate full DDL matrices here; link to the cookbook when operators need tuning guidance.
<span id="operating-at-scale-v19"></span>
## Operating at scale (v1.9+)
v1.9 adds the **telemetry operator narrative**, the **audit table indexing cookbook**, and **production checklist** guidance on **volume, retention cadence, and purge monitoring** — this heading is a **map** to those homes, not a second copy of their tables or matrices.
- **[Telemetry (operator reference)](#telemetry-operator-reference)** — `:telemetry` events operators should chart.
- **[Trigger coverage (operational)](#trigger-coverage-operational)** — how `Threadline.Health.trigger_coverage/1` tuples relate to `mix threadline.verify_coverage` and on-call triage.
- **[Audit table indexing cookbook](audit-indexing.md)** — baseline vs optional indexes and join semantics for timeline, export, and correlation workloads.
- **[Production checklist — retention and volume](production-checklist.md#4-retention-and-purge)** — purge cadence, growth signals, and CLI/API gates (see **`### Volume, growth, and purge cadence`** under §4).
## Brownfield continuity
Tables with **pre-existing rows** still use **T0** semantics: `Threadline.history/3` may return `[]` until the first trigger-backed mutation after capture is installed. Operators should follow [`guides/brownfield-continuity.md`](brownfield-continuity.md) for checklists, `mix threadline.verify_coverage`, and `mix threadline.continuity` (including `--dry-run`).
## AuditAction
`AuditAction` rows represent application-level audit events you insert via `Threadline.record_action/2`. They are independent of trigger capture until you associate them with transactions through `action_id`.
## AuditContext
`AuditContext` is built by `Threadline.Plug` (or your own code) and stored on `conn.assigns`. It is not persisted until you bridge actor identity into the database inside a transaction (see `Threadline.Plug`).
## ActorRef
`ActorRef` is the structured actor representation serialized to JSON for `audit_transactions.actor_ref` and `audit_actions.actor_ref`. Use `Threadline.Semantics.ActorRef.to_map/1` with `Jason.encode!()` when setting the GUC.
## Telemetry (operator reference)
Threadline emits **`:telemetry.execute/3`** events (no attached handler is required for correctness). Attach handlers in your application `Application.start/2` (or equivalent) for metrics and logs.
| Event | When | Measurements | Metadata |
|-------|------|--------------|----------|
| `[:threadline, :transaction, :committed]` | After capture commits work, or as a proxy when `Threadline.record_action/2` succeeds without an explicit post-commit hook | `table_count` (non‑neg integer; accurate only if you call `Threadline.Telemetry.transaction_committed/2` after the transaction) | `%{}` |
| `[:threadline, :action, :recorded]` | After `Threadline.record_action/2` finishes (success or failure) | `status` (`:ok` or `:error`) | `%{}` |
| `[:threadline, :health, :checked]` | After `Threadline.Health.trigger_coverage/1` returns | `covered`, `uncovered` (counts of tables in each bucket) | `%{}` |
### `[:threadline, :transaction, :committed]`
**When it fires.** Threadline emits this event after capture-associated transactions commit their work, and also emits it as a **proxy** with `table_count: 0` when `Threadline.record_action/2` succeeds without an explicit post-commit hook that supplies real per-transaction counts.
**What to measure.** Use `table_count` when you need fidelity to “how many distinct audited tables produced rows in this transaction.” Compare week-over-week after deploys or schema changes.
**Metadata.** Handlers receive an empty map (`%{}`) today; keep dashboards tolerant if metadata keys are added later.
**Misleading or degraded signals.** `table_count` is often **0** on the `record_action` proxy path even when semantic capture succeeded — that is not proof that triggers failed. A generic smell: steady **zero** `table_count` while application traces show audited-table writes for hours → confirm whether events are dominated by the proxy vs missing `Threadline.Telemetry.transaction_committed/2` after `Repo.transaction/1`.
**Where to look next.** [`production-checklist.md` §1 — Capture and triggers](production-checklist.md#1-capture-and-triggers) for install / `mix threadline.gen.triggers` / coverage cadence; [`production-checklist.md` §6 — Observability](production-checklist.md#6-observability) for handler wiring.
### `[:threadline, :action, :recorded]`
**When it fires.** Immediately after `Threadline.record_action/2` completes, success or failure.
**What to measure.** Emit rate split by `status` (`:ok` vs `:error`). Error spikes often track validation failures, missing `ActorRef`, or repo outages — chart both absolute errors and error ratio.
**Metadata.** Empty map (`%{}`).
**Misleading or degraded signals.** High `:ok` traffic does **not** imply every domain table row was captured; this event tracks the semantics helper, not each physical mutation.
**Where to look next.** [`production-checklist.md` §1 — Capture and triggers](production-checklist.md#1-capture-and-triggers) for trigger coverage cadence; [`production-checklist.md` §2 — Actor bridge and semantics](production-checklist.md#2-actor-bridge-and-semantics) for GUC / `record_action` pairing.
<span id="threadline-health-checked"></span>
### `[:threadline, :health, :checked]`
**When it fires.** After `Threadline.Health.trigger_coverage/1` returns from its catalog pass.
**What to measure.** `covered` and `uncovered` are **aggregate counts** of tables in each bucket across the public user tables `Health` enumerates — telemetry does not stream per-table tuples here.
**Metadata.** Empty map (`%{}`).
**Misleading or degraded signals.** A rising `uncovered` count is inventory drift, not automatically a CI failure: `mix threadline.verify_coverage` enforces only the configured `expected_tables` intersection (see [`## Trigger coverage (operational)`](#trigger-coverage-operational)).
**Where to look next.** Tuple-level results and Mix policy live under [`## Trigger coverage (operational)`](#trigger-coverage-operational); operational cadence in [`production-checklist.md` §1 — Capture and triggers](production-checklist.md#1-capture-and-triggers).
**Weekly / post-deploy / “metrics look wrong” triage**
1. Confirm `:telemetry` handlers for Threadline events are attached in the running release ([`production-checklist.md` §6 — Observability](production-checklist.md#6-observability)).
2. For `[:threadline, :transaction, :committed]`, sample `table_count`: persistent zeros during known writes usually mean the proxy path or missing `Threadline.Telemetry.transaction_committed/2` — revisit the subsection above and `Threadline.Telemetry` on HexDocs.
3. For `[:threadline, :action, :recorded]`, compare `:ok` vs `:error` trends against deploys and auth incidents ([`production-checklist.md` §2 — Actor bridge and semantics](production-checklist.md#2-actor-bridge-and-semantics)).
4. For `[:threadline, :health, :checked]`, reconcile `uncovered` with [`## Trigger coverage (operational)`](#trigger-coverage-operational) before tuning alerts.
5. After schema or trigger changes, rerun the §1 checklist items for `mix threadline.gen.triggers` and `mix threadline.verify_coverage` ([`production-checklist.md` §1 — Capture and triggers](production-checklist.md#1-capture-and-triggers)).
6. Remember **retention purge** does not emit these events — use purge batch logs, not this triage list, when investigating purge-only windows.
7. If correlation-scoped investigations spike, verify whether `record_action` volume alone explains `transaction_committed` traffic (proxy vs counted commits).
8. After material Plug/Phoenix changes, re-check actor GUC wiring and telemetry boot order together (§1 + §6).
**Retention purge** does not emit these events today; use application logs from `mix threadline.retention.purge` / `Threadline.Retention.purge/1` (see task `@moduledoc`) or wrap purge calls with your own telemetry.
See also: `Threadline.Telemetry` on HexDocs for copy-paste attach examples.
## Trigger coverage (operational)
`Threadline.Health.trigger_coverage/1` takes **`repo:`** (required `Ecto.Repo` module) and returns a list of tagged tuples:
`[{:covered, String.t()} | {:uncovered, String.t()}]`
Each tuple names a **public** user table the catalog query sees. `{:covered, name}` means Threadline’s `threadline_audit_*` trigger was found on that relation; `{:uncovered, name}` means it was not.
**Audit catalog tables.** `audit_transactions`, `audit_changes`, and `audit_actions` are **excluded** from the per-table list — they are not expected to carry capture triggers (CAP-10 / `Threadline.Health` `@moduledoc`). Do not expect them in `Health` output.
**`mix threadline.verify_coverage`.** Hosts configure `config :threadline, :verify_coverage, expected_tables: [...]` with the audited tables CI must protect. The task calls `Threadline.Health.trigger_coverage/1`, then `Threadline.Verify.CoveragePolicy.violations/2`, which applies **intersection semantics:** only names in `expected_tables` can fail the Mix task. A `{:uncovered, table}` tuple for a table **not** listed in `expected_tables` is informative output, not a Mix failure by itself.
**Telemetry link.** When you need how those aggregate counts surface in metrics, see the [`[:threadline, :health, :checked]`](#threadline-health-checked) subsection under [`## Telemetry (operator reference)`](#telemetry-operator-reference).
## Correlation
**Correlation is not a database table** in Threadline. Correlation identifiers flow through headers (`x-correlation-id`), assigns, and optional fields on `AuditAction`. Treat them like trace context: they stitch logs and actions across boundaries without implying a `correlations` schema.
<span id="exploration-api-routing-v110"></span>
## Exploration API routing (v1.10+)
This block answers **“which public API first?”** for common exploration tasks. **SQL, golden queries, and subsection detail** live under **[Support incident queries](#support-incident-queries)** — use that section when you need copy-paste SQL or full filter vocabulary.
Contract marker for automated doc checks: **XPLO-03-API-ROUTING**
| Intent | Primary API | Notes / pointer |
|--------|---------------|-----------------|
| Single domain row over time | `Threadline.history/3` or `Threadline.timeline/2` | `history/3` lists changes for one PK; use `timeline/2` when you need the shared filter map (`:table`, `:from`, `:to`, …). **T0 / brownfield:** rows that existed before capture may look empty until the first audited write — see [`brownfield-continuity.md`](brownfield-continuity.md) and **[Brownfield continuity](#brownfield-continuity)** in this guide. |
| Incident / time window across rows | `Threadline.timeline/2` or `Threadline.timeline_page/2` | Use eager `timeline/2` for smaller bounded windows. Switch to `timeline_page/2` for large investigations where stable traversal across pages matters; bounds still apply to `AuditChange.captured_at` (see [subsection 1](#1-row-history-pk-changes-in-a-time-window)). |
| Correlation-scoped slice | `Threadline.timeline/2`, `Threadline.timeline_page/2`, `Threadline.Export`, `mix threadline.export` | Pass **`:correlation_id`**; timeline/export return only changes whose transaction **inner-joins** an `audit_actions` row with that correlation — see [subsection 3](#3-correlation-bundle-shared-correlation_id). |
| Everything in one DB transaction | `Threadline.incident_bundle/2` | Default transaction drill-down when you want linked transaction/action context plus ordered changes with packaged diffs. |
| Field-level diff for one `%AuditChange{}` | `Threadline.change_diff/2`, `Threadline.ChangeDiff` | Advanced building block for custom projections on top of `audit_changes_for_transaction/2` or `incident_bundle/2`; INSERT/UPDATE/DELETE semantics still live in the module docs. |
| Actor-scoped window (optional) | `Threadline.actor_history/2`, `Threadline.timeline/2` or `Threadline.timeline_page/2` with **`:actor_ref`** | Pairs with support table row 2; use the paged path when the actor window is too large for one eager list; SQL in [subsection 2](#2-actor-window-one-actor-across-tables). |
<span id="time-travel-as-of-v120"></span>
## Time Travel (As-of)
This hub maps the single-row **`as_of/4`** contract for operators who need one historical snapshot fast.
Contract marker for automated doc checks: **ASOF-06**
| Behavior | Result |
|----------|--------|
| Default call | Returns the stored snapshot as a **map**. |
| Deleted record | Returns an explicit deleted-record error instead of a fake struct. |
| Genesis gap | Returns an explicit genesis gap error when no historical row exists yet. |
| `cast: true` | Reifies into the current schema via `Ecto.embedded_load/3`; unknown keys are ignored and cast failures return `{:error, {:cast_error, message}}`. |
Use this when you need a one-row reconstruction by primary key. For a copy-paste walkthrough, see [the Phoenix example README](../examples/threadline_phoenix/README.md#historical-reconstruction-walkthrough).
<span id="example-incident-json-v111"></span>
### Reference example: incident JSON (v1.11+)
Contract marker for automated doc checks: **COMP-EXAMPLE-INCIDENT-JSON**
The path-dependent Phoenix app under **`examples/threadline_phoenix/`** shows the
canonical bundled incident path on top of the table above:
1. **`POST /api/posts`** returns **`audit_transaction_id`** — the UUID of the **`audit_transactions`** row for that HTTP request’s database transaction (after `Threadline.record_action/2` links semantics in the same transaction as in prior phases).
2. **`GET /api/audit_transactions/:id/changes`** renders **`Threadline.incident_bundle/2`** for that transaction, returning linked transaction/action context plus ordered change rows with packaged **`change_diff`** payloads suitable for JSON incident tools.
If you need a custom projection instead of the bundled default, the lower-level
building blocks remain public: **`Threadline.audit_changes_for_transaction/2`**
preserves the ordering contract, **`Threadline.transaction_context/2`** exposes
the linked context directly, and **`Threadline.change_diff/2`** lets you shape
per-row diffs yourself.
CI covers the round-trip in **`ThreadlinePhoenixWeb.PostsIncidentJsonPathTest`**.
The reference app requires an authenticated actor before it serves the
drill-down endpoint. Production hosts still own tenancy scoping and any richer
authorization policy beyond that baseline.
## Support incident queries
SQL-native operator playbooks for the five canonical support questions (see `.planning/milestones/v1.8-REQUIREMENTS.md`, “Evidence-driving questions”). Run against a **read-only** session or **replica** when possible. Example SQL uses placeholder schema **`your_schema`** — replace it (and any `your_table` / PK literals) with your install’s names before executing.
**Replace before run:** `your_schema` → audited schema (often `public`); `your_table` / PK values → the row under investigation; time literals → bounded window; `your_correlation_id` → trace string from logs.
Contract marker for automated doc checks: **LOOP-04-SUPPORT-INCIDENT-QUERIES**
| # | Question | Primary path |
|---|----------|--------------|
| 1 | Row history — what changed for this domain row (PK) in the last N days? | `Threadline.history/3` or `Threadline.Query.timeline/2` — SQL: [subsection 1](#1-row-history-pk-changes-in-a-time-window) |
| 2 | Actor window — what did this actor drive across tables in a time window? | `Threadline.actor_history/2` or `Threadline.timeline/2` / `Threadline.timeline_page/2` with `:actor_ref` — SQL: [subsection 2](#2-actor-window-one-actor-across-tables) |
| 3 | Correlation bundle — row-level changes and semantic actions sharing a correlation id | `Threadline.timeline/2` / `Threadline.timeline_page/2` / export with `:correlation_id` — SQL: [subsection 3](#3-correlation-bundle-shared-correlation_id) |
| 4 | Export parity — same slice for review and export | `Threadline.Export`, `mix threadline.export` — details: [subsection 4](#4-export-parity-timeline-and-export-filters-agree) |
| 5 | Action ↔ capture — tie semantic actions to captured mutations | Join `audit_actions` ↔ `audit_transactions` — SQL: [subsection 5](#5-action-and-capture-link-semantic-actions-to-changes) |
### 1. Row history - PK changes in a time window
| Path | When to use it |
|------|----------------|
| **API** | `Threadline.history(MyApp.Schema, id, repo: MyApp.Repo)` returns `AuditChange` structs for one PK; use `Threadline.timeline/2` when you need the shared filter map (`:table`, `:from`, `:to`, …). |
| **SQL** | Ad-hoc psql / BI — join `audit_changes` to `audit_transactions`, constrain `table_name`, JSON containment on `table_pk`, and **bounded** `captured_at`. |
When **`:from`** / **`:to`** are set on `timeline/2` or `timeline_page/2`, bounds apply to **`AuditChange.captured_at`** (inclusive). Prefer **`LIMIT`** in raw SQL during exploration.
**Replace before run:** `your_schema`, `your_table`, PK map, timestamps.
```sql
SELECT ac.id,
ac.transaction_id,
ac.table_schema,
ac.table_name,
ac.op,
ac.captured_at,
ac.table_pk,
ac.changed_fields
FROM your_schema.audit_changes ac
JOIN your_schema.audit_transactions at ON at.id = ac.transaction_id
WHERE ac.table_name = 'your_table'
AND ac.table_pk @> '{"id": 123}'::jsonb
AND ac.captured_at >= '2026-04-01T00:00:00Z'::timestamptz
AND ac.captured_at <= '2026-04-24T23:59:59Z'::timestamptz
ORDER BY ac.captured_at DESC, ac.id DESC
LIMIT 500;
```
### 2. Actor window - one actor across tables
| Path | When to use it |
|------|----------------|
| **API** | `Threadline.actor_history/2` lists transactions for one `ActorRef`; combine with `Threadline.timeline/2` for smaller windows and `Threadline.timeline_page/2` for large windows when you need change rows across tables. |
| **SQL** | Filter `audit_transactions.actor_ref` (JSON) or join through capture rows — keep a **time bound** on `at.occurred_at` or `ac.captured_at`. |
**Replace before run:** `your_schema`, actor JSON literal, window bounds.
```sql
SELECT ac.id,
ac.table_name,
ac.op,
ac.captured_at,
ac.table_pk
FROM your_schema.audit_changes ac
JOIN your_schema.audit_transactions at ON at.id = ac.transaction_id
WHERE at.actor_ref @> '{"kind": "user", "id": "user-uuid-here"}'::jsonb
AND ac.captured_at >= '2026-04-20T00:00:00Z'::timestamptz
AND ac.captured_at <= '2026-04-24T23:59:59Z'::timestamptz
ORDER BY ac.captured_at DESC
LIMIT 500;
```
### 3. Correlation bundle - shared correlation_id
| Path | When to use it |
|------|----------------|
| **API** | `Threadline.timeline/2`, `Threadline.timeline_page/2`, `Threadline.Export` / `mix threadline.export` with **`:correlation_id`** in the filter list (same key as timeline). |
| **SQL** | Mirror library semantics with an **inner join** to `audit_actions` on the transaction’s `action_id`. |
**Strict semantics:** when **`:correlation_id`** is set to a non-empty string, **timeline** and **export** return only `audit_changes` whose **`audit_transactions`** row links to an **`audit_actions`** row with that **`correlation_id`** (via `action_id`). Capture rows for transactions **without** that action link **do not** appear — there is no “include orphan capture” mode for this filter. Omit `:correlation_id` entirely to leave correlation out of the filter (export may still `LEFT JOIN` actions for metadata without changing which changes match).
**Replace before run:** `your_schema`, `your_correlation_id`.
```sql
SELECT ac.id,
ac.table_name,
ac.op,
ac.captured_at,
ac.table_pk,
aa.id AS audit_action_id,
aa.correlation_id
FROM your_schema.audit_changes ac
JOIN your_schema.audit_transactions at ON at.id = ac.transaction_id
JOIN your_schema.audit_actions aa
ON aa.id = at.action_id
AND aa.correlation_id = 'your_correlation_id'
ORDER BY ac.captured_at DESC, ac.id DESC
LIMIT 500;
```
### 4. Export parity - timeline and export filters agree
| Path | When to use it |
|------|----------------|
| **Mix / API** | **`mix threadline.export`** (see task `@moduledoc`) and `Threadline.Export.to_csv_iodata/2`, `to_json_document/2`, `stream_changes/2` — same allowed keys as `Threadline.Query.timeline/2`. |
| **SQL** | Use when validating parity in the database; **replicate the same predicates** you pass to `timeline/2` (table, actor, time bounds, correlation inner join when filtering by correlation). |
Unknown filter keys raise **`ArgumentError`** in both code paths — see `Threadline.Query` moduledoc.
### 5. Action and capture - link semantic actions to changes
| Path | When to use it |
|------|----------------|
| **API** | `Threadline.record_action/2` sets semantic intent; capture links when the transaction’s `action_id` points at the `audit_actions` row driving that transaction. |
| **SQL** | Start from `audit_actions`, join `audit_transactions`, then `audit_changes`. |
**Replace before run:** `your_schema`, `your_action_id`.
```sql
SELECT aa.id,
aa.name,
aa.correlation_id,
at.id AS transaction_id,
ac.id AS change_id,
ac.table_name,
ac.op,
ac.captured_at
FROM your_schema.audit_actions aa
JOIN your_schema.audit_transactions at ON at.action_id = aa.id
JOIN your_schema.audit_changes ac ON ac.transaction_id = at.id
WHERE aa.id = 999001
ORDER BY ac.captured_at DESC
LIMIT 500;
```