Skip to content

Read a plan with --explain

In Build & run Clinker you ran --explain and saw a few lines of output. Let’s actually read it. It’s the single best way to understand a pipeline before it runs, and your first glimpse of an idea that shapes the whole engine: Clinker plans a job before it runs it.

  • Read an execution plan top to bottom and name what each line reports: DAG nodes, transform count, mode, arbitration.
  • Name the four nodes of customer_etl’s DAG and say which kinds count as transforms and which don’t.
  • Predict a plan from the YAML alone (its node count, its mode, its arbitration) before running --explain.
  • Distinguish a change that reshapes the plan (a new node, a memory policy) from one that doesn’t (overriding a variable’s value).

New terms in this lesson (each is named here before it appears below):

  • plan: the compiled, validated form of a job, printed by --explain.
  • DAG, the directed, acyclic graph the nodes wire into.
  • node: one step in that graph; only some kinds are transforms.
  • tier, a dependency level in the source DAG (Tier 0 reads first).
  • streaming mode: records flow straight through, not collected-then-emitted.
  • arbitration, the memory policy the run would apply under pressure.

You read the four-node customer_etl pipeline back in the build & run lesson. That’s enough to call its plan before you run a thing.

Terminal window
cd examples/pipelines
cargo run -p clinker -- run customer_etl.yaml --explain

--explain takes the YAML all the way through compilation (parsing, validation, typechecking the CXL, building the graph) and then prints the result instead of executing it. Nothing is read from the source; nothing is written. Here’s the heart of the output:

=== Resolved Outputs ===
'results' → ./output/customers.csv
=== Execution Plan ===
Mode: Streaming
Transforms: 2
Output projections: 1
DAG nodes: 4
arbitration: BackPressurePreferred -> Priority
Source DAG:
Tier 0: customers
Transform 'active_only':
Parallelism: Stateless
Transform 'final_flag':
Parallelism: Stateless

Read it top to bottom:

  • Resolved Outputs: where results will land (./output/customers.csv).
  • DAG nodes: 4, the source, two transforms, and the output, wired into a directed acyclic graph.
  • Mode: Streaming, so records can flow straight through without the engine having to collect them all first.
  • Source DAG / Tier 0: customers: the one source sits at tier 0, the first (and here only) wave of sources to read.
  • Parallelism: Stateless (per transform): each row is independent, so these transforms have no cross-record state.
  • arbitration: BackPressurePreferred -> Priority, the memory policy the run would use if it came under pressure. You’re seeing the engine’s bounded-memory machinery named, before a byte is allocated.

--explain even prints a Physical Properties section with each node’s predicted peak memory (for this tiny job, predicted_peak=345B, the size of the input CSV).

That a plan exists as its own thing, printable, inspectable, produced before execution, is not an accident. Compilation turns the YAML into a typed, validated artifact, and only that artifact is handed to the runtime:

clinker-plan ·compiled.rs ·CompiledPlan type @19acdcb4
pub struct CompiledPlan {
dag: ExecutionPlanDag, // the lowered execution graph
config: PipelineConfig, // the validated configuration
// …plus the validated compile outputs the runtime reads (typechecked CXL, bound schemas)
// ...
}

We won’t unpack CompiledPlan yet; that’s a Planning & Expressions lesson. For now, just hold the shape of the idea: plan first, run second. --explain is you stepping in between the two.

You can read the plan as given. Now turn it into a skill: change one thing, predict how the plan shifts (or doesn’t), and verify. Each rung uses a real pipeline that ships in examples/pipelines/.

You already did this above. Confirm you can answer, without re-running it, the three checkpoint-grade questions about customer_etl’s plan: how many DAG nodes (4), how many transforms (2), what mode (Streaming). If any of the three isn’t instant yet, re-read the output once more before moving on; the next two rungs build on reading this fluently.

Modify: change one thing, predict the new plan

Section titled “Modify: change one thing, predict the new plan”

Here’s the question that separates operating the flag from reading the engine: which changes reshape the plan, and which don’t? Two modifications, opposite answers.

Modify A: override a variable’s value. customer_etl declares a variable, gold_threshold (default 10000), and a channel can override it per tenant. There is no --var CLI flag; the override is a .channel.yaml overlay you pass with --channel. Acme’s channel sets the threshold to 50000:

channels/acme-corp/customer_etl.channel.yaml
channel:
name: acme-corp
target: ./customer_etl.yaml
vars:
static:
gold_threshold: { type: int, default: 50000 }
Terminal window
cargo run -p clinker -- run customer_etl.yaml \
--channel channels/acme-corp/customer_etl.channel.yaml --explain

Modify B: change the memory policy. Now a change that does move the plan. The scd_type2.yaml example declares a memory budget with a backpressure policy:

pipeline:
name: scd_type2_backfill
memory: { limit: "16K", backpressure: spill }
Terminal window
cargo run -p clinker -- run scd_type2.yaml --explain

Create: predict a plan from scratch, then verify

Section titled “Create: predict a plan from scratch, then verify”

No worked answer this time. The tumbling_clicks.yaml example is a three-node pipeline: a source, an aggregate that buckets clicks into hourly windows, and an output. You have not seen its plan.

That’s Orientation. You can build the engine, run a pipeline, work the compiler loop, navigate the crates, and read a plan, and predict one from the YAML before you run it. Next comes A Record’s Journey, where we follow one record all the way through that four-node DAG and start opening the doors.

From the pipeline author’s side (optional, one-directional; the same --explain plan read from the pipeline author’s side, no engine internals):