diff --git a/crates/plan-runner/docs/diagrams/make_figures.sh b/crates/plan-runner/docs/diagrams/make_figures.sh
new file mode 100755
index 0000000..6d30150
--- /dev/null
+++ b/crates/plan-runner/docs/diagrams/make_figures.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+# You need to have Graphviz installed to run this script
+# On Debian-based OSes, you can install it using: sudo apt-get install graphviz
+
+# Directory containing .dot files. Defaults to the script's own directory so the
+# script works regardless of the caller's working directory.
+SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+ASSET_DIR=${1:-"${SCRIPT_DIR}"}
+
+# Make figures from .dot files
+for f in "${ASSET_DIR}"/*.dot; do
+ dot -Tsvg "$f" -o "${f%.dot}.svg"
+done
diff --git a/crates/plan-runner/docs/diagrams/workflow.dot b/crates/plan-runner/docs/diagrams/workflow.dot
new file mode 100644
index 0000000..831c94b
--- /dev/null
+++ b/crates/plan-runner/docs/diagrams/workflow.dot
@@ -0,0 +1,136 @@
+digraph PlanRunnerWorkflow {
+fontname = "Helvetica,Arial,sans-serif"
+layout = dot
+rankdir = LR
+ranksep = 0.9;
+nodesep = 0.7;
+splines = true;
+compound = true;
+bgcolor = "white"
+
+node [
+fontname = "Helvetica,Arial,sans-serif",
+shape = box,
+style = "filled,rounded",
+color = "#555555",
+fillcolor = "white",
+penwidth = 1.5
+]
+edge [
+fontname = "Helvetica,Arial,sans-serif",
+color = "#333333",
+fontsize = 9,
+fontcolor = "#555555",
+labeldistance = 2.0,
+penwidth = 1.2
+]
+
+subgraph cluster_input {
+label = "Input"
+style = "dashed"
+color = "#888888"
+fontcolor = "#555555"
+margin = 18
+json_plan [label = <
+| JSON Plan |
+| • schema: name -> arity |
+| • facts: name -> rows |
+| • query: { root, nodes } |
+| • expected_bindings (optional oracle) |
+
>, fillcolor = "#E8F4FD", color = "#2196F3"]
+}
+
+subgraph cluster_parse {
+label = "Parse"
+style = "dashed"
+color = "#9C27B0"
+fontcolor = "#7B1FA2"
+margin = 14
+parse_plan [label = "parse_plan(json)\n-> Plan", fillcolor = "#F3E5F5", color = "#9C27B0"]
+}
+
+subgraph cluster_load {
+label = "Load Tables (--backend selects the path)"
+style = "dashed"
+color = "#4CAF50"
+fontcolor = "#388E3C"
+margin = 14
+build_pure [label = <
+| build_tables(plan) |
+| --backend memory |
+| direct from plan.facts |
+
>, fillcolor = "#E8F5E9", color = "#4CAF50"]
+build_storage [label = <
+| build_tables_via_storage<S: Storage> |
+| --backend memory-storage |
+| --backend lmdb / redb / fjall |
+| --backend sqlite / geomerge |
+| create_relation → tx.insert → scan_as_table |
+
>, fillcolor = "#E8F5E9", color = "#4CAF50"]
+tables_map [label = <
+| HashMap<String, Table> |
+| positional rows per relation |
+
>, fillcolor = "#E8F4FD", color = "#2196F3"]
+}
+
+subgraph cluster_execute {
+label = "Execute (walk node DAG in id order)"
+style = "dashed"
+color = "#FF9800"
+fontcolor = "#F57C00"
+margin = 14
+execute_node [label = <
+| execute(tables, query) |
+| Action::Scan → scan_atom |
+| Action::Join Left → semijoin(l, r) |
+| Action::Join Right → semijoin(r, l) |
+| Action::Join Natural → natural_join(l, r) |
+| cache per-node Relation; return root |
+
>, fillcolor = "#FFF3E0", color = "#FF9800"]
+relation_out [label = <
+| Relation |
+| columns: variables + wildcards |
+| rows: bindings |
+
>, fillcolor = "#FFF3E0", color = "#FF9800"]
+}
+
+subgraph cluster_verify {
+label = "Verify (when expected_bindings is present)"
+style = "dashed"
+color = "#9C27B0"
+fontcolor = "#7B1FA2"
+margin = 14
+verify_node [label = <
+| verify(plan, relation) |
+| project to expected.columns |
+| multiset compare against expected.rows |
+
>, fillcolor = "#F3E5F5", color = "#9C27B0"]
+}
+
+subgraph cluster_output {
+label = "Output"
+style = "dashed"
+color = "#888888"
+fontcolor = "#555555"
+margin = 18
+stdout_json [label = <
+| stdout JSON |
+| { columns, rows } |
+
>, fillcolor = "#ECEFF1", color = "#607D8B"]
+oracle_pass [label = "Ok(true) / VerifyError\n(used by tests/examples.rs)", fillcolor = "#ECEFF1", color = "#607D8B"]
+}
+
+// Pipeline edges
+json_plan -> parse_plan [color = "#2196F3"]
+parse_plan -> build_pure [label = "Backend::Memory", color = "#9C27B0"]
+parse_plan -> build_storage [label = "Backend::*Storage", color = "#9C27B0"]
+build_pure -> tables_map [color = "#4CAF50"]
+build_storage -> tables_map [color = "#4CAF50"]
+tables_map -> execute_node [color = "#2196F3"]
+parse_plan -> execute_node [style = "dashed", label = "plan.query", color = "#9C27B0"]
+execute_node -> relation_out [color = "#FF9800"]
+relation_out -> stdout_json [color = "#607D8B"]
+relation_out -> verify_node [style = "dashed", color = "#FF9800"]
+parse_plan -> verify_node [style = "dashed", label = "plan.expected_bindings", color = "#9C27B0"]
+verify_node -> oracle_pass [color = "#9C27B0"]
+}
diff --git a/crates/plan-runner/docs/diagrams/workflow.svg b/crates/plan-runner/docs/diagrams/workflow.svg
new file mode 100644
index 0000000..11a0ee4
--- /dev/null
+++ b/crates/plan-runner/docs/diagrams/workflow.svg
@@ -0,0 +1,202 @@
+
+
+
+
+