# reactive_dag v0.17.0-rc.40 - Table of Contents

> Reactive DAG engine as an Ash extension: dirty frontier, depth-ordered incremental drain, change propagation. Author nodes as Ash resources with reduce/join/aggregate combinators; each node's results are its own rows. You declare the relationships; one engine runs them.

## Pages

- [reactive_dag](readme.md)

- Guides
  - [Getting started](getting-started.md)
  - [Configuration](configuration.md)
  - [Authoring nodes](authoring-nodes.md)
  - [Human augmentation](human-augmentation.md)
  - [LLM nodes](llm-nodes.md)
  - [Sources and scanning](sources.md)
  - [One engine, and where the domain enters](seams.md)

- Design
  - [ADR-001 — Extract the reactive-DAG engine as reactive_dag](adr-001-reactive-dag-library.md)

## Modules

- [ReactiveDag.Basis](ReactiveDag.Basis.md): A **content-addressed digest of a row set** — what those rows looked like at a
moment, so a later comparison can tell whether they have moved.
- [ReactiveDag.Config](ReactiveDag.Config.md): Boot-time validation of `config :reactive_dag, …`.
- [ReactiveDag.Drain.Report](ReactiveDag.Drain.Report.md): What a drain ACTUALLY did — the processing trace, returned by
`ReactiveDag.Drain.run/2`.
- [ReactiveDag.DrainWorker](ReactiveDag.DrainWorker.md): The Oban job that drains the frontier and nothing else.
- [ReactiveDag.Dsl](ReactiveDag.Dsl.md): The DSL compile pipeline over the flat `Cell` IR — the resolve→lower→validate
machinery both host DSLs share, parameterized by app hooks so each keeps its
own domain vocabulary.
- [ReactiveDag.Frontier](ReactiveDag.Frontier.md): The dirty frontier, owned by the library and backed by the `reactive_dag_dirty`
table (created by `ReactiveDag.Migration`). The host is an Ash/AshPostgres app,
so we go through its repo with raw SQL — values always parameterized; the
table name (the one identifier SQL cannot parameterize) comes from config and
is validated against an identifier grammar at read time, so a typo fails
loudly instead of as a syntax error deep in a query. Claim-as-delete is a raw
`DELETE … RETURNING` that Ash actions don't express cleanly.
- [ReactiveDag.Insights](ReactiveDag.Insights.md): The engine, viewed from outside: what the graph LOOKS like, what state each
cell is in, and what the last drains actually did.
- [ReactiveDag.Job](ReactiveDag.Job.md): What every library-provided Oban job needs from its arguments.
- [ReactiveDag.KeyRule](ReactiveDag.KeyRule.md): The vocabulary of propagation: when `changed` keys of a child feed a parent,
which of the parent's keys become dirty?
- [ReactiveDag.Migration](ReactiveDag.Migration.md): The library-owned DDL, callable from a host migration — the dirty-frontier
table `ReactiveDag.Frontier` reads and writes (coalesced by `(cell_id, key)`;
claim is a `DELETE … RETURNING`)
- [ReactiveDag.Node.Aggregate](ReactiveDag.Node.Aggregate.md): A PURE-ASH-QUERY reduce: the datastore does the grouping via a RELATIONSHIP
aggregate. The node's own resource is the group's resource — ONE row per group
— and `over` names its `has_many` to the rows being aggregated. The library loads
the aggregates in ONE Ash query — Postgres computes the `GROUP BY` — and each
parent row's aggregate values are its payload. No rows cross into the BEAM; no
`into`/`read`/`upsert` (contrast the in-BEAM `reduce`, which loads every row).
- [ReactiveDag.Node.Changes.MarkDirty](ReactiveDag.Node.Changes.MarkDirty.md): Marks a written record's key dirty on its own cell, so the next drain picks
the change up. Wired automatically by `dirties_on` — hosts do not add it.
- [ReactiveDag.Node.Compose](ReactiveDag.Node.Compose.md): An anonymous nested op-expression leg: composes inline as an intermediate
cell (its `as` id, or a positional id derived from the parent). Its own legs
are `ref`/`compose`, so the algebra reads as an expression tree.

- [ReactiveDag.Node.Compute](ReactiveDag.Node.Compute.md): The ESCAPE HATCH: declare an arbitrary recompute MODULE (a `ReactiveDag.Op`)
for a node whose computation the `reduce`/`join` combinators can't express —
an LLM call, a PDF/Tigris fetch, a bespoke multi-input recompute. `compute
MyApp.EventsExtract` sits in the block alongside the combinators, mirroring
Ash's `calculate :x, :type, MyModule` (the arbitrary case is an entity too,
not a schema key beside the declarative ones).

- [ReactiveDag.Node.Context](ReactiveDag.Node.Context.md): A by-name CONTEXT input edge (`context :people`): the node READS the target
as settled context but is NOT recomputed when the target changes. Still a
real input (validated, ordered by depth so the target settles first, read
at recompute) — it just doesn't propagate. For a node whose recompute is
expensive/non-deterministic and consults mutable context it shouldn't be
re-triggered by (an LLM step that looks up a human-curated
people/positions table). Contrast `ref`, which dirties this node on change.

- [ReactiveDag.Node.Fingerprint](ReactiveDag.Node.Fingerprint.md): The one value that decides whether an observation MOVED.
- [ReactiveDag.Node.Join](ReactiveDag.Node.Join.md): A declarative JOIN: read ONE input's payload, index it into a LEFT and a
RIGHT side (each a `%{join_key => item}` built from a per-side key fn), then
emit one row per left key joined to its right item (right may be absent). The
common declared-vs-observed reconcile/variance shape — the author writes the
two side keys + the join row, not the read/write/changed plumbing.
- [ReactiveDag.Node.Lapse](ReactiveDag.Node.Lapse.md): What a machine recompute does to a HUMAN's mark: `lapse :approved_at,
when_changed: :any`.
- [ReactiveDag.Node.PerKey](ReactiveDag.Node.PerKey.md): The PER-ENTRY MAP: for each claimed row of the input, call a generic action
with that row and write its structured output into this node's attributes.
- [ReactiveDag.Node.Poll](ReactiveDag.Node.Poll.md): This node's rows come from OUTSIDE the graph: `poll MuniWatch.Crawler`.
- [ReactiveDag.Node.Recompute.Declarative](ReactiveDag.Node.Recompute.Declarative.md): The pure builders behind the DECLARATIVE combinator slots. Each turns a
declarative spec — attribute atoms, fold keywords, side picks — into the
same fn shape the per-slot escape hatches supply, so the recompute pipeline
runs one code path however declarative the author went.
- [ReactiveDag.Node.Recompute.PerKey](ReactiveDag.Node.Recompute.PerKey.md): Runs a `per_key` node: for each claimed input row, call a generic action with
that row and write its structured output into this node's attributes.
- [ReactiveDag.Node.Recompute.Read](ReactiveDag.Node.Recompute.Read.md): Executes a combinator's READ — always an Ash read of the over node's
resource (its primary read action, or the `:read` action `read:` names),
shaped by the optional `query:` transformer, and ALWAYS scoped by the
library: when the recompute claimed specific dirty keys, the over's payload
key is filtered to them (the transformer cannot un-scope; scoping is the
substrate's correctness concern, not policy).
- [ReactiveDag.Node.Recompute.Union](ReactiveDag.Node.Recompute.Union.md): Runs a `union` node: one row per `(input cell, key)` across several inputs,
written into this node's own table.
- [ReactiveDag.Node.RecomputeBy](ReactiveDag.Node.RecomputeBy.md): THE declaration the engine cares about: **what unit does a change
invalidate?** Everything else a combinator declares — `group_by`, `into`,
key derivation — is mapping data into shape once you already know what to
recompute.
- [ReactiveDag.Node.Reduce](ReactiveDag.Node.Reduce.md): A declarative REDUCE (fold): read an input node's payload, group it, and
reduce each group to one output row — the common map/fold shape, so the
author writes the grouping + reduction, not the read/write/changed plumbing.
Anything the combinator can't express (an LLM call, an external fetch, a
bespoke join) uses the `compute:` module escape hatch instead.

- [ReactiveDag.Node.Ref](ReactiveDag.Node.Ref.md): A by-name input edge to another named node (`ref :id`). The general form —
nestable inside `compose`. The flat `depends_on: [:a, :b]` schema key is sugar
that lowers to one `%Ref{}` per id.
- [ReactiveDag.Node.Rows](ReactiveDag.Node.Rows.md): Reads a cell's own rows, keyed the way the DAG keys them.
- [ReactiveDag.Node.Run](ReactiveDag.Node.Run.md): The ASH-NATIVE escape hatch: `run :recompute_keys` declares that this
node's recompute is a GENERIC action on its own resource — one step less
escape than a `compute` module, because the computation stays a first-class
Ash action (arguments, policies, `Ash.run_action` testability).
- [ReactiveDag.Node.Slice](ReactiveDag.Node.Slice.md): A dimension a human may select this node by: `slice :fiscal_year`.
- [ReactiveDag.Node.Transformers.AddMarkDirty](ReactiveDag.Node.Transformers.AddMarkDirty.md): Wires `dirties_on` and `augmented_by` onto `ReactiveDag.Node.Changes.MarkDirty`,
so ordinary Ash writes trigger the cascade with no host boilerplate.
- [ReactiveDag.Node.Union](ReactiveDag.Node.Union.md): The N-INPUT shape: one row per `(input cell, key)` across several inputs,
materialised into this node's own table.
- [ReactiveDag.Node.Verifiers.VerifyReactive](ReactiveDag.Node.Verifiers.VerifyReactive.md): Compile-time checks for the `reactive` block — everything verifiable against
the node's OWN resource fails at `defmodule`, not at drain time. (Checks that
need the OVER node's resource — named read actions, attribute existence —
run at graph assembly instead: that is the earliest point cross-node facts
are known.)

- [ReactiveDag.ReprocessWorker](ReactiveDag.ReprocessWorker.md): Re-derive a cell's rows without any input having changed — *the code moved,
not the data*.
- [ReactiveDag.Rollup](ReactiveDag.Rollup.md): Summing one key across many meta maps — the arithmetic behind every "what did
this cost" line, wherever the numbers came from.
- [ReactiveDag.ScanRun](ReactiveDag.ScanRun.md): What one scan DID — the poll and the drain it triggered, as a single value.
- [ReactiveDag.ScanWorker](ReactiveDag.ScanWorker.md): The Oban job that polls one scanner and drains what it changed.
- [ReactiveDag.Verdict](ReactiveDag.Verdict.md): A cell's live verdict: the one-word answer, rolled up from its rows' statuses.

- Authoring
  - [ReactiveDag.Calendar](ReactiveDag.Calendar.md): Calendar bucketing as an ASH CALCULATION — the Ash-native answer to the
classic "date-marked records → time-bucketed aggregate". The bucket is
declared on the resource that OWNS the date (where derived values live in
Ash), and a rollup node just groups by the calculation
  - [ReactiveDag.Node](ReactiveDag.Node.md): An **Ash resource extension** that makes a resource a node in a reactive DAG.
The resource IS the node **and** its own payload table: its `reactive` block
defines the computation, its `attributes` are the rows the node materializes.
This is the intended shape — one resource, both roles.
  - [ReactiveDag.Node.Payload](ReactiveDag.Node.Payload.md): Closes the payload loop for a resource-backed node: writes a combinator's output
row into the node's OWN resource (`cell.meta.resource`), keyed by the cell key.
  - [ReactiveDag.Node.Recompute.Aggregate](ReactiveDag.Node.Recompute.Aggregate.md): Runs a pure-Ash-query `aggregate` node: the datastore groups + aggregates the
node's `over` relationship in ONE query (a relationship aggregate — Postgres does
the `GROUP BY`), and each parent row's aggregate values become its payload.

- Getting data in
  - [ReactiveDag.Op](ReactiveDag.Op.md): The behaviour a node's `compute` module implements — the recompute for ONE op,
the per-cell unit of work.
  - [ReactiveDag.Source](ReactiveDag.Source.md): A **scanner** — how the world gets in.

- Engine
  - [ReactiveDag.Cell](ReactiveDag.Cell.md): One node in the DAG — the domain-neutral IR both host apps compile down to.
  - [ReactiveDag.Drain](ReactiveDag.Drain.md): The reactive propagation loop — the heart of the substrate, shared by both
hosts.
  - [ReactiveDag.Graph](ReactiveDag.Graph.md): Pure DAG construction: a list of `ReactiveDag.Cell` → a `ReactiveDag.Plan`.
  - [ReactiveDag.Lowering](ReactiveDag.Lowering.md): Shared machinery for lowering a NESTED op-expression into a flat cell list —
the recursion both host DSLs independently grew (cascade's `Lower.resolve_legs`,
the portal's `Graph.build_node`). Same algorithm: walk an op's legs, recurse
into each, a `ref` resolves to an existing cell-id (no new cell), a nested op
becomes an intermediate cell whose inputs are the recursed leg ids.
  - [ReactiveDag.Node.KeyRule](ReactiveDag.Node.KeyRule.md): THE propagation rule: how a change reaches a parent, decided by what the parent
DECLARED. `ReactiveDag.Drain` calls this — there is nothing to configure.
  - [ReactiveDag.Node.Recompute](ReactiveDag.Node.Recompute.md): THE engine: how a cell recomputes, decided by what its node DECLARED.
  - [ReactiveDag.Plan](ReactiveDag.Plan.md): The compiled DAG plan — pure data the drain executes. Decoupled from any DSL:
a host app lowers its declarations into a `Cell` list and `Graph.build/1`
produces this. The drain only ever sees the Plan.

- Exceptions
  - [ReactiveDag.Config.Error](ReactiveDag.Config.Error.md): Raised by `ReactiveDag.Config.validate!/0` when configuration is wrong.
  - [ReactiveDag.Drain.RunawayError](ReactiveDag.Drain.RunawayError.md): The drain exceeded its pass budget — likely a cycle, or a recompute that
keeps re-dirtying its own inputs. `:report` carries the PARTIAL trace up to
the abort: `report.steps`' tail shows exactly which cells keep triggering
each other, which is the diagnostic for the loop this error suspects.

