Understanding Directed Acyclic Graphs (DAGs)
Imagine a structured business pipeline where you cannot ship a product until it has been inspected, and you cannot inspect it until it has been built. If you tried to inspect it before building it, the system would collapse.
A DAG (Directed Acyclic Graph) is the mathematical and architectural model used to design these exact types of workflows. It is the core framework driving modern data engineering pipelines (like Airflow, dbt, or Prefect), compiler optimization, and agent orchestration.
π¬ Breaking Down the Acronym
- Graph: A collection of nodes (representing steps or tasks) connected by edges (representing dependencies or paths).
- Directed: The edges have a clear direction of execution. Task A points to Task B ($A \rightarrow B$). You must complete $A$ before starting $B$.
- Acyclic: No loops (cycles). This is the most crucial constraint. You can never traverse the graph and find yourself back where you started. If Task B pointed back to Task A, the pipeline would enter a logical infinite loop, preventing it from ever finishing.
π» A Simple DAG Example in Python
In software engineering, when we want to execute a DAG, we use a process called Topological Sorting. This algorithm figures out the exact execution order to guarantee that no task runs before its dependencies are met.
Here is a pure Python implementation of a DAG execution engine (using only the standard library) to solve a classic pipeline: Processing a Web File.
from collections import deque
class TaskDAG:
def __init__(self):
# Nodes mapped to a list of other nodes that depend on them
self.graph = {}
# Every node mapped to its number of incoming dependencies (in-degree)
self.in_degree = {}
def add_task(self, task):
if task not in self.graph:
self.graph[task] = []
self.in_degree[task] = 0
def add_dependency(self, parent, child):
"""Specifies that 'parent' must execute before 'child'."""
self.add_task(parent)
self.add_task(child)
self.graph[parent].append(child)
self.in_degree[child] += 1
def execute_pipeline(self):
"""Performs a Topological Sort (Kahn's Algorithm) to execute tasks in order."""
# Find all tasks that have zero dependencies to start with
queue = deque([task for task, degree in self.in_degree.items() if degree == 0])
execution_order = []
while queue:
current_task = queue.popleft()
execution_order.append(current_task)
# Process all tasks that depend on the current task
for dependent in self.graph[current_task]:
self.in_degree[dependent] -= 1
# If all its dependencies have been executed, it is ready to run
if self.in_degree[dependent] == 0:
queue.append(dependent)
# Cyclic check: If the execution order doesn't match the total nodes, a loop exists!
if len(execution_order) != len(self.graph):
raise ValueError("Error: Cyclical dependency detected! This graph is NOT a DAG.")
return execution_order
# --- RUNNING THE PIPELINE ---
dag = TaskDAG()
# Define our workflow dependencies:
# 1. We must download data before parsing it
dag.add_dependency("Download raw CSV", "Parse CSV into JSON")
# 2. We must parse the data before we can analyze it OR generate a PDF
dag.add_dependency("Parse CSV into JSON", "Analyze user metrics")
dag.add_dependency("Parse CSV into JSON", "Generate PDF report")
# 3. We must complete the analysis before we email the team
dag.add_dependency("Analyze user metrics", "Email metrics to sales team")
# 4. We must generate the PDF before uploading it to OneDrive
dag.add_dependency("Generate PDF report", "Upload PDF to OneDrive")
# 5. We must complete the OneDrive upload before we email the team
dag.add_dependency("Upload PDF to OneDrive", "Email metrics to sales team")
try:
plan = dag.execute_pipeline()
print("π¦ Optimal Execution Path:")
for idx, step in enumerate(plan, 1):
print(f" {idx}. {step}")
except ValueError as e:
print(e)
Output of the Execution:
π¦ Optimal Execution Path:
1. Download raw CSV
2. Parse CSV into JSON
3. Analyze user metrics
4. Generate PDF report
5. Upload PDF to OneDrive
6. Email metrics to sales team
Where you’ve already met DAGs
DAGs are the underlying logic in tools you use daily: JavaScript bundlers like Vite resolve import DAGs to compile assets in dependency order, git commit histories are DAGs (merges give a commit two parents; nothing ever points back), make builds the target/prerequisite DAG before running a single recipe, and AI agent planners draft DAGs of tool execution to solve user tasks β the planning pattern in Choosing-the-Right-Agentic-Design-Pattern works precisely because an articulable task structure is a DAG.
The acyclic constraint is the load-bearing part. If a planner puts a cycle in its reasoning (Tool A calls Tool B, which calls Tool A), the system grinds to a halt β that is the “ReAct looping excessively” failure signal from the pattern note, expressed as a graph property. Designing execution as a clean DAG keeps it deterministic, parallelizable, and debuggable.