TransformGENERALTOOL-SPECIFIC

DAGs in Data Pipelines

Node, edge, no cycles — the whole structure. Why every question a data platform asks about itself turns out to be a standard graph traversal, and why a cycle is almost always a modelling error.

Who needs this, what one row is, and why the obvious build breaks

Every lesson starts from the consumer, because designing from the source outward is this domain's characteristic mistake.

The question

Why is the dependency graph of a data platform required to be acyclic, and what does a cycle actually tell you when you find one?

Who needs this

Anyone who needs the platform to answer a question about itself: what runs first, what can run together, what does this change affect, and where did this number come from. All four are graph questions and none of them is answerable without the graph.

What one row is

A node is one unit of work with one output — a task, or a dataset-with-its-producing-transformation. An edge is one directed dependency: B reads what A wrote. Edges have no weight, no timing and no semantics beyond "after" (DAG (Directed Acyclic Graph)).

The obvious build

Model the pipeline as a list of steps in order. Step one, step two, step three. It is how the work was described in the ticket, it is how a scheduled script executes, and it is correct for any pipeline that genuinely is a line.

Why it breaks

The moment two steps are independent, the list forces them to run one after the other for no reason, and the pipeline is as slow as the sum of its parts rather than the length of its longest chain (Topological Execution).

How it breaks with real data
  • The moment two steps are independent, the list forces them to run one after the other for no reason, and the pipeline is as slow as the sum of its parts rather than the length of its longest chain (Topological Execution).
  • A step is added that depends on two earlier ones. A list can express "after step four" but not "after both step two and step four", so the ordering is enforced by putting it late enough and hoping (Task Dependencies).
  • A step fails. A list has no notion of which later steps are still safe to run, so the whole thing stops, including the branch that had nothing to do with the failure (When a Task Fails Mid-DAG).
  • Someone asks what a change to step two affects. A list says "everything after it", which is technically true and useless — most of what comes after does not read it (Impact Analysis).
  • Two steps end up mutually dependent through a third. A list cannot represent it, so it happens by accident and manifests as one of them reading stale data on every run, forever, without an error.
SourceIngestionRawTransformationValidationStorage ModelServingConsumerObservability

What is actually happening

  • A directed acyclic graph is the minimum structure that can express "after" without expressing "when". That distinction is the whole point: dependency is a statement about correctness, scheduling is a statement about time, and conflating them is what makes cron-driven pipelines fragile (Scheduler vs Orchestrator).
  • Acyclicity is what makes the graph *executable*. A cycle means there is no order in which every node runs after its dependencies, because each is waiting on the other. Detection is the same traversal that produces the ordering — a topological sort that cannot consume every node has found a cycle (Topological Sort).
  • The standard algorithms map directly onto operational questions. Kahn's algorithm, repeatedly taking nodes with no unmet dependencies, gives both a valid execution order and the natural parallel batches. A depth-first traversal with a colouring scheme detects cycles and yields the reverse-postorder ordering (DFS Topological Sort).
  • Forward reachability from a node is its blast radius; reverse reachability is its provenance. These are the same traversal on the same graph with the edges flipped, which is why a platform that can answer one can always answer the other (Depth-First Search (DFS)).
  • Longest-path reasoning over the DAG — the critical path — is what determines end-to-end build time, and it is the only quantity that adding workers cannot improve. This is a dynamic-programming computation over the topological order, not a search (DP on DAGs).

The whole structure, and the four questions it answers

A DAG has exactly three rules. Edges point one way. Following edges never returns you to where you started. That is all — there are no weights, no durations and no conditions in the structure itself, and everything operational is layered on top of it.

What makes it worth the formality is that four questions a data platform is constantly asked turn out to be the same small set of traversals. That is not a coincidence: these are the questions dependency structure can answer, and a platform that models dependency gets all four for the price of one.

The mapping below is worth internalising because it tells you what to expect from a graph and what not to. Every row is a structural question. None of them is a question about data, and no traversal of any graph will ever tell you that a number is wrong.

Operational questionGraph operationWhat it gives youWhat it cannot tell you
What order does the build run in?Topological sort (Topological Sort)A sequence in which every node follows everything it reads. Any valid order is as correct as any other.How long it will take, or whether any node should run at all today.
What can run at the same time?Repeatedly take all nodes with no unmet dependencies (Kahn's algorithm)The parallel batches, in order. Width per batch is the parallelism actually available.Whether your warehouse has the concurrency to use it, or whether running them together causes contention.
I am changing this model — what breaks?Forward reachability from the node (Depth-First Search (DFS))The descendant set: the exact blast radius, including consumers two layers away that nobody remembers (Impact Analysis).Whether the change is actually breaking. The graph gives reach, not semantics.
This number is wrong — where did it come from?Reverse reachability: the same traversal on flipped edgesThe ancestor set, walked in order, giving the list of places to check (Lineage Debugging).Which ancestor is the guilty one. That still requires looking at the data at each node.
Why is the build so slow?Longest path, computed over the topological order (DP on DAGs)The critical path — the chain that decides end-to-end time and that parallelism cannot shorten (Amdahl's Law).Which node on that path is worth optimising, which needs per-node cost and duration data.
Is this graph even runnable?Cycle detection — a topological sort that cannot consume every node (DFS Topological Sort)A yes/no answer before anything executes, plus the nodes involved in the cycle.What the cycle *means*, which is almost always a modelling question rather than a scheduling one.
        raw_orders          raw_refunds        raw_customers
             |                   |                   |
        stg_orders           stg_refunds         stg_customers
             \                  /                   /
              \                /                   /
               int_orders_enriched  <-------------'
                        |
                    fct_orders
                    /         \
        customer_metrics    revenue_daily
                    \         /
                     dashboards

  in-degree 0  -> sources: nothing upstream, must be ingested
  out-degree 0 -> leaves: consumed directly, or dead work
  longest path -> critical path: the floor on end-to-end freshness
  width        -> the most nodes that can ever run at once

A cycle is a modelling error, not a scheduling one

GENERALThe claim that cycles are modelling errors holds for transformation graphs, where nodes are datasets. In task-oriented orchestration a cycle can occasionally be a legitimate retry or sensor loop expressed badly, and those tools provide explicit constructs for it rather than edges.

When a transformation graph has a cycle, the instinct is to treat it as an execution problem: break the loop, read one side's previous version, carry on. That resolves the symptom and preserves the cause, and the cause is nearly always that two datasets have been given a definition that depends on each other.

The most common shape is a metric feeding back into its own input. customer_metrics computes a customer segment; someone then wants fct_orders to carry the segment, so it joins customer_metrics; now the fact table depends on an aggregate of itself. Nothing about that is a scheduling problem — the model has claimed that a customer's segment is both an input to and an output of order history.

The honest resolutions are all modelling moves. Split the node so the cyclic part is a separate, explicitly time-lagged dataset — segment as of yesterday, materialised and named as such. Or push the shared logic upstream into a node both can read. Or accept that the value is genuinely recursive and compute it iteratively outside the graph, publishing each generation as its own dataset.

What all three have in common is that the time lag becomes visible and named. The bad fix hides it: a node quietly reading its own previous output produces numbers that are one generation stale in a way that appears nowhere, and that nobody can reconstruct six months later (Semantic Changes).

Cycles, and what each one is really telling you
TriggerSymptomCauseResponse
fct_orders joins customer_metrics, which aggregates fct_orders.Parse fails with a cycle, or — worse — the platform resolves it by reading yesterday's customer_metrics.A customer attribute has been defined as both an input to and a derivative of order history. The model, not the scheduler, is contradictory.Materialise the segment as an explicitly dated snapshot — dim_customer_segment_daily — and join on the date. The lag is now named, testable and visible to consumers (Snapshot Tables).
Two staging models each enrich from the other to fill missing fields.A cycle between two nodes that neither author intended, each reasonable in isolation.The same reconciliation logic was written twice, in both directions, instead of once upstream.Extract the shared logic into a node both read. The cycle disappears because the mutual dependency was never real — it was duplicated logic (Model Layering).
A running total that reads its own previous partition.No cycle in the graph at all, because the dependency is on yesterday's output of the same node.A genuine recurrence. This is legitimate and is *not* a cycle — it is a self-edge across time, which a DAG per-run can represent perfectly well.Keep it, but make the dependency on the prior partition explicit and make the node idempotent, or a re-run compounds the total (Idempotent Data Pipelines).
A model reads a table that a downstream job writes back into.The graph looks acyclic because the write-back is not a modelled node — it is a script, or a reverse-ETL sync.The real dependency graph includes systems outside the project. The tool's graph is a subgraph and is silently incomplete.Bring the write-back into the graph as a node, or forbid it. A table with two producers has no ordering guarantee at all (Source of Truth).
A cycle appears only in production, not in development.Development builds a subset of the graph and never sees the edge that closes the loop.Environment-dependent references — a model that resolves to a different upstream by environment.Parse the full graph in CI regardless of what gets built. Cycle detection is a parse-time check and costs nothing to run on everything.

Nodes as tasks, nodes as datasets

The same word covers two different graphs, and confusing them causes real arguments. An orchestrator's DAG has tasks as nodes: run this script, call this API, wait for this file. A transformation framework's DAG has datasets as nodes: this relation, produced by this query.

The difference is what an edge means. A task edge means "start after this finished" and carries no claim about data. A dataset edge means "reads the contents of" and is therefore also a lineage edge, an impact-analysis edge and a documentation edge. That is why a transformation graph can generate lineage and a task graph cannot: the task graph never knew what the tasks touched (Data Lineage).

Neither is a superset. A task graph can express work that produces no relation — copying a file, calling an external system, sending a notification — which a dataset graph has no way to represent. In practice a platform has both, with the orchestrator's graph containing the transformation project as a single node, and the boundary between them is exactly where dependency information gets lost (Orchestration).

Two graphs that both get called "the DAG"
One graph of tasks, dependencies declared by hand
Every transformation is a task. Ordering between tasks is written out explicitly by whoever added them. Lineage is a separate diagram maintained separately, and impact analysis is a conversation.
A task graph containing a dataset graph
The orchestrator holds coarse cross-system dependencies — ingestion finished, then transform, then export — and the transformation project derives its own internal dataset graph from code, including lineage and selective rebuild.

Declared dependencies are a second source of truth about what reads what, and second sources of truth drift the moment a model changes without its declaration being updated — after which the ordering is wrong in a way that produces successful runs over stale inputs. A derived dataset graph cannot drift, because the edges are parsed from the same code that executes. The orchestrator is still needed for the dependencies that are not data reads at all — an external file arriving, an API export finishing — which no amount of SQL parsing can discover (Task Dependencies).

How to build it

Most important first.

  • Model dependency as data reads, not as timing. "This runs after that" invites a cycle; "this reads that" cannot produce one unless the data model genuinely has one (Dependency Graphs: The Real Shape of Your Code).
  • Let the graph be derived from the code wherever possible. A hand-declared graph is a second source of truth, and second sources of truth drift (dbt Concepts).
  • Fail the build on a cycle rather than resolving it automatically. A cycle is information — it says the model is wrong — and silently breaking it with a stale read hides the finding.
  • Keep nodes at the granularity you would want to retry. Too coarse and a retry redoes correct work; too fine and the graph is larger than the problem (Retries in Pipelines).
  • Represent cross-system dependencies explicitly — an ingestion job, an external file arriving — rather than approximating them with a start time. A schedule that "usually runs late enough" is a dependency with no edge (Orchestration).

What this actually promises

Naming the guarantee you do not have is worth more than naming the one you do — everything downstream inherits the weakest promise in the chain.

  • The graph guarantees that a valid execution order exists and that any topological order is as correct as any other. Which one you get is not specified and must not matter.
  • It guarantees that a cycle is detectable before anything runs, which is the cheapest possible moment to find one.
  • It guarantees nothing about time. There is no promise that a node runs at a particular hour, within a duration, or after an external system has finished — those are scheduling and sensing concerns layered on top (Task Dependencies).
  • It guarantees nothing about data. A perfectly ordered traversal over stale, empty or duplicated inputs completes successfully and reports success (The Pipeline Succeeded. The Data Is Wrong.).

Can I trust it?

A green pipeline is evidence that code ran. These four fields are the evidence that the data is right.

The check that would catch this
  • Assert that the graph is acyclic at parse time, and assert that every node has at least one test. The second assertion is the one people skip and the one that reveals how much of the graph is unguarded.
  • Assert that every external dependency has an explicit edge or an explicit sensor. A node whose real dependency is a time-of-day guess will eventually run early, and it will succeed when it does.
  • None of this checks data. Graph-level assertions are about structure; a structurally perfect graph over wrong inputs produces wrong outputs in the correct order (Data Tests).
Freshness
  • The floor on end-to-end freshness is the critical path: the longest chain of dependent nodes. No amount of parallelism reduces it, which makes graph depth a freshness decision made at modelling time (Amdahl's Law).
  • Adding a node in the middle of the longest path costs every downstream consumer its build time. Adding a node on a side branch costs nobody anything.
  • A dependency on an external system that is represented as a wait rather than as an edge inflates freshness by the size of the safety margin someone guessed at.
When the schema or meaning changes
  • Adding an edge is safe for correctness and costly for freshness — it can extend the critical path and it can serialise two nodes that used to run together.
  • Removing an edge is the dangerous direction. If the dependency was real and undocumented, removing it produces a node that reads stale data successfully, with no error at any point (Stale Dashboards).
  • Splitting a node into two changes the retry granularity and the failure surface for everyone downstream, which is a behavioural change even when the outputs are byte-identical.
How to re-run this safely
  • Recovery uses the same traversal as execution: from the failed node, take the descendant set and rebuild it in topological order. Ancestors are known-good and rebuilding them is waste (Reprocessing vs Retrying).
  • Retrying a single node is safe only if the node is idempotent. The graph guarantees ordering, not repeatability, and a node that appends will happily append twice (Idempotent Data Pipelines).
  • A partial descendant rebuild is the failure to guard against: it leaves the graph internally inconsistent, with two nodes at different vintages, which reads as a data bug rather than as an incomplete recovery (Validating a Backfill Before You Publish).

What can go wrong

Failure modes
  • A cycle introduced through a third node, invisible to review because no two files mention each other.
  • A missing edge, so a node runs before its real upstream and reads the previous run's output — successfully, every time.
  • A dependency approximated by a start time, which works until the upstream is slow once.
  • A node with two producers, only one of which is in the graph, so the graph's ordering guarantee simply does not apply to that table.
  • The mitigation failing: cycle detection that runs at execution time rather than at parse time, so the cycle is discovered by a build that has already published half a graph.
Misreads
  • "A DAG is a schedule." It is an ordering constraint. Nothing in it says when anything runs, and a platform that conflates the two ends up with dependencies expressed as start times (Scheduler vs Orchestrator).
  • "Cycles are a scheduling problem to work around." A cycle in a transformation graph nearly always means the model is wrong — see the section below. Breaking it with a stale read converts a detectable design error into a permanent silent one.
  • "Any topological order will do, so ordering does not matter." Any order is *correct*; orders differ enormously in wall-clock time and in how early a failure is discovered. Correctness and cost are separate questions here (Topological Execution).
  • "The graph shows me the data flow." It shows dependency between datasets. Which *columns* flow where is a strictly finer graph and a much harder one to produce (Column-Level Lineage).

Operating it

How you see it in production
  • The graph itself, rendered, with last-run status per node. It is the single most-used debugging artifact on any data platform that has one (Data Observability).
  • Critical-path duration over time. It grows silently as nodes are added and it is what a freshness SLO is actually made of (The Freshness SLO).
  • Nodes with in-degree zero that are not sources, and out-degree zero that are not consumed. Both are usually mistakes: the first is a missing edge, the second is dead work (Compute Waste).
What changes at 10x and 100x
  • At tens of nodes, ordering is the value. At hundreds, reachability is the value — nobody can hold the descendant set of a staging model in their head.
  • Parallel width caps out at the graph's width, so beyond a point adding workers changes nothing and the only remaining lever is restructuring the graph (Parallelism Moves the Load Downstream).
  • Cross-team graphs are where the model breaks down organisationally rather than technically: an edge between two teams' nodes is a coordination obligation, and nothing in the graph enforces it (Data Ownership).
What drives cost here
  • Graph *shape* decides how much parallelism is available and therefore how much of the build is wall-clock time versus compute time. A wide graph finishes sooner at the same total cost (Why Eight Cores Give You Four and a Half).
  • The cost of a change is the size of its descendant set, which is a modelling property. Wide fan-out from a shared upstream makes every change expensive (Impact Analysis).
  • Graph maintenance itself is close to free when derived and expensive when declared, because a declared graph must be kept true by people.
What this approach costs
  • A DAG cannot express iteration or feedback, and that limitation is deliberate — it is what buys guaranteed termination and a computable order. Pipelines that genuinely need a loop must express it outside the graph, usually as a separate run.
  • Fine-grained nodes give precise retries and precise attribution, and cost scheduling overhead and a graph that is harder to read. The right granularity is the retry granularity.
  • Deriving the graph from code guarantees it matches reality and constrains how transformations may be written. Declaring it allows anything and guarantees nothing.

DAG cycle detector

Change an input and watch which number moves — and which one does not. Everything here comes from a model in this repository, not from a measurement.

DAG cycle detector
The A in DAG is the whole contract. A graph with a cycle has no valid run order, and the scheduler's only honest response is to refuse.
Models
14
Scheduled
14sim
Never ready
0sim
Verdict
acyclic
wave 1raw_ordersraw_customersraw_events
wave 2stg_ordersstg_customersstg_events
wave 3int_sessionsint_order_itemsdim_customer
wave 4fct_ordersmart_basket
wave 5mart_revenue_dailymart_conversion
wave 6exec_dashboard
Every model has a wave. Nodes in the same wave depend on nothing in that wave, so they can run at the same time — which is what makes a topological sort worth computing rather than just a run order to validate.
GENERALKahn's algorithm run over the model graph: repeatedly take every node whose dependencies are all built. A node that is never ready is in a cycle, or downstream of one.

Where this applies

Almost nothing here is universal. These labels say what each claim is specific to, and where a different engine, format, warehouse or scale would differ.

  • GENERALNodes, edges, acyclicity and the traversals over them are mathematical properties and hold identically in every orchestrator and transformation framework. What varies is whether nodes are tasks or datasets, and that distinction changes what an edge means operationally.
  • TOOL-SPECIFICOrchestrators model nodes as tasks with side effects, while transformation frameworks model nodes as datasets with a producing query. A task graph can express "run this script" and cannot derive lineage; a dataset graph derives lineage and cannot express work that produces no relation.

Where the depth lives

This domain teaches how data moves and how you know it arrived intact. It hands the rest off by name.

Domains that do not exist yet
  • DevOps / Production Engineering owns the same structure applied to builds and deployments — build dependency graphs, deployment ordering and what a partially applied rollout leaves behind. The traversals are identical; only the nodes differ.