๐งน Best Practices
Conventions adopted across dagpipe pipelines. Follow these to keep graphs deterministic, observable, and debuggable.
๐งฑ Node design
- Keep nodes pure. A node must never mutate its input
Stateโ fork a new one instead. Inputs are shared across branches, so mutation would corrupt siblings. - Yield zero states to prune.
return ()inresolvekills the branch: children never run and this path contributes no terminal state. - One responsibility per node. Name nodes for what they do
(
entity.resolve_numeric_merchant,text.normalize), mirroring theirfile.modulelocation. - Prefer stateless subclasses. The singleton-per-subclass model means a
stateless node can be reused everywhere for free. Only declare
__init__when you need per-run dependencies (then inspect how instances are created โ custom__init__opts out of the singleton). - Use
Node.forkinsideresolveso the node ID is recorded inhistoryautomatically.
๐ฏ State & schema design
- Validate as early as possible. A root
Statevalidates its payload in__post_init__โ construct roots inside a factory with a clear error path. - Declare the schema once at the
Schema-building layer and reuse it across state subclasses and YAML definitions. - Use
objectsparingly. Free-formobjectfields defeat validation. Prefer explicit types or unions (str | None) wherever the contract is known. - Evolve schemas deliberately.
fork()updates are path-validated; adding a new path is a breaking change for existing payloads.
๐ Graph patterns
- Multiple roots are independent. They receive the same root state and share nothing โ don't expect cross-communication between roots.
- Merge = shared child. To join branches, point all of them at the same node. The merge node runs once per incoming state.
- Prune early with guards.
if not state.get(...): returninresolvekeeps downstream work minimal. - Keep graphs acyclic by construction. Cycle detection exists, but a cycle is always a design bug. Lay out your graph like a DAG from the start.
- Prefer programmatic
Graphfor dynamic topologies; use YAML only for static pipelines you can diff in review.
โก Async guidance
- Mix sync and async nodes freely โ dispatch is per node, not per engine.
- Wrap per-node I/O in
AsyncNode.resolve_async; keep the engine generic. - Don't
await run_async(...)for all-sync graphs;runis cheaper.
๐ช Step-wise guidance
resume_fromis 0-based โ persistlast_index + 1and resume from it.- Treat
completed=Falseas "no output", not "failure". - Route
ProgressMessagethroughon_stephooks for tracing/progress bars without coupling the engine.
๐งช Testability
- Build synthetic
Statevia a small factory (see themake_statefixture pattern in Testing) โ one call per scenario. - Assert on terminal states and their
history, not intermediate prints. - For YAML pipelines, test with a tiny importable node module and a
tmp_pathfixture (as the integration suite does), keeping tests offline.
โ Anti-patterns
| Pattern | Why it's wrong |
|---|---|
Mutating state.payload or state.metadata |
Breaks immutability; shared across branches |
Building nodes with heavy __init__ |
Skips singleton reuse; couples pipeline to instance state |
Logging inside resolve |
Hard to test; pollutes output. Use on_step hooks instead |
Reusing one Graph across concurrent runs |
Assumes nodes are stateless โ they must be |
๐ Read Next
- Error Handling โ the exceptions you'll actually see.
- 07 โ Step-wise execution โ resume & hooks.
- 07 โ Testing example โ the fixture pattern.