Flow
What you’ll learn: how to write, run, and debug a Flow — a stateful causal stream function with typed ports and one synchronous step() — as plain Python, before any backend, camera, or robot is involved.
The smallest shape
Section titled “The smallest shape”A Flow subclasses Flow[Input, Output] and overrides step(). Inputs and outputs are @io dataclasses; their fields are the graph ports.
@io turns the class into a dataclass whose fields are all Optional and carry present/absent signal information. Flow[NumberInput, NumberOutput] is the type boundary Retriever checks: both parameters must be @io types (or None for a source or sink). step() is the local computation — synchronous, and deterministic whenever its input and local state are.
Run it like ordinary Python
Section titled “Run it like ordinary Python”A Flow is callable directly. No pipeline, no clock, no event loop:
That direct call is the point. A Flow should be unit-testable and breakpoint-friendly on its own.
Ports are optional; _signals says what arrived
Section titled “Ports are optional; _signals says what arrived”Because @io makes every field Optional, a Flow can receive a partial input — some ports present, others not yet. _signals lists the fields that are actually set, so step() can branch on what it got instead of assuming a full observation.
Local state with reset()
Section titled “Local state with reset()”Runtime-local state — counters, buffers, model handles — belongs in reset(), not __init__(). The runtime calls reset() once when a run starts and again on Pipeline.reset().
Keep __init__() lightweight and serializable. Backends reconstruct a Flow from its init_config(), so put devices, sockets, and SDK clients in reset() and expose only plain constructor arguments through init_config().
Run the shipped example
Section titled “Run the shipped example”This runs examples/tutorial/a_flow_fundamentals/01_basic_flow.py, which exercises both DoubleFlow and the _signals branch and prints each step.
The same shape scales to robot policies
Section titled “The same shape scales to robot policies”A VLA, VLM, planner, state estimator, or controller is still one Flow at the runtime boundary: one typed snapshot in, one typed output out. The model can be arbitrarily heavy inside step(); the graph owns timing and data handoff around it.
Put timing in the Pipeline, not the Flow
Section titled “Put timing in the Pipeline, not the Flow”A Flow never decides when it runs. That belongs to the graph. You attach a clock with @ and declare how each edge is sampled with sync=:
This split is the whole design: a Flow owns local computation; the graph owns when the Flow wakes up and which upstream record it consumes. Keep out of a Flow anything that belongs to the graph or runtime — do not start a private event loop for timing, do not read “whatever message last arrived” from global state, and do not fold backend launch, logging, or replay into the model logic.
Next: Time and Sync shows how clocks and edge sync policies build the input passed to step().
