1. Portable Functions
Your functions should look the same whether they run inside hypergraph or not. The@node decorator adds metadata; it should not force a framework-specific coding style.
Good example:
- Function logic requires framework state object structure to work
- Removing
@nodebreaks core business logic
2. Zero Ceremony
Graph complexity should match problem complexity, not framework requirements. No state schemas. No manual edge wiring for ordinary data flow.Data edges are inferred by matching output and input names. Manual wiring is only needed for renaming or adapting to different contexts.
- Adding adapter/plumbing nodes only to satisfy orchestration mechanics
3. Names Are Contracts
Edges are inferred from matching output and input names. Output names are API contracts.- Invalid names (
"bad-output", keyword output names) - Ambiguous naming collisions
4. Validate Early, Fail Clearly
Structural errors should fail when building the graph, not while a long run is in progress.Caught at Build Time
Duplicate node names, invalid targets, missing inputs, type mismatches
Clear Error Messages
Helpful diagnostics that point to the exact issue
- Duplicate node names
- Invalid graph/node/output names
- Shared-parameter default consistency
- Invalid gate targets and gate self-targeting
multi_targetoutput conflicts- Disallowed cache usage on unsupported node types
- Strict type compatibility when enabled
5. Composition Over Configuration
When workflows grow, nest graphs instead of adding flags or ad-hoc configuration surfaces. A graph is a node: build and test independently, then reuse via.as_node().
- Forcing unrelated concerns into one flat, hard-to-reason-about graph
6. Keep Routing Simple
Routing nodes decide where execution goes, not what heavy computation happens. Good example:- Doing expensive LLM/tool work inside a routing function
- Returning undeclared targets
Routing should be quick and based on already-computed values. Async routing functions and generator routing functions are rejected.
7. Cycles Require Entry Points
When a value participates in a cycle, the first iteration needs an initial value. Entry points group these by the node where execution starts.- Running cyclic flows without initializing entrypoint values
8. Immutability
Nodes and graphs behave like values. Transformations produce new instances.with_*, bind, select, and unbind are documented as immutable transformations.- Assuming
graph.bind(...)mutates in place and discarding the returned graph
9. Explicit Over Implicit
Be explicit about outputs, renames, and control flow intent.- Relying on hidden conventions instead of explicit naming and routing declarations
10. Think Singular, Scale with Map
Write logic for one item. Scale orchestration with.map() or GraphNode.map_over(...).
Singular Logic
Each function processes one item, keeping it simple and testable
Framework Scaling
Use map_over or runner.map() to fan out over collections
- Embedding manual batch loops into node logic instead of mapping execution
11. Caching Is Opt-In and Deterministic
Caching should depend on stable node definition + input values.Cache key includes
definition_hash and resolved inputs. Code changes invalidate cache naturally via definition hash changes.- Requires node opt-in (
cache=True) and a runner cache backend GraphNodecaching is disallowedInterruptNodedefaults tocache=Falsebut is configurable- Gate routing decisions can be cached and replayed safely
12. Use .bind() for Shared Resources
Provide stateful/non-copyable dependencies through bindings, not mutable signature defaults.
Good example:
13. Separate Computation From Observation
Execution logic and observability should stay decoupled.Runners emit structured lifecycle events. Processors are best-effort; observability should not alter business logic.
- Embedding logging/telemetry control flow directly into node computation
14. One Framework, Full Spectrum
The same primitives (@node, @route, Graph, runners) span DAGs, branches, loops, and nested workflows.
Common Design Dilemmas
| Dilemma | Option A | Option B | Prefer | Why |
|---|---|---|---|---|
| Function design | Framework-coupled state dict | Plain function params/returns | B | Keeps portability and local testability |
| Growing complexity | Flat mega-graph | Nested graphs via .as_node() | B | Better encapsulation and reuse |
| Validation timing | Catch during execution | Catch at Graph(...) construction | B | Faster feedback, lower run-time risk |
| Scaling strategy | Manual loops in nodes | .map() / map_over | B | Cleaner node semantics |
| Shared dependencies | Signature defaults for clients/connections | .bind() shared resources | B | Correct lifecycle and copy semantics |
The Underlying Test
A design likely fits hypergraph when:- Functions are testable as plain Python
- Graph wiring mostly comes from meaningful names
- Structural mistakes surface before execution
- Nested composition reduces complexity
- Diffs track business logic, not framework plumbing