Flows let you build multi-screen terminal applications by chaining screens together with the>>operator. Each screen has its own template, reducer, and state slice — Milo handles navigation and state routing.
Defining screens
AFlowScreenbundles a name, template, and reducer:
from milo.flow import FlowScreen
def welcome_reducer(state, action):
if state is None:
return {"message": "Welcome to the installer"}
if action.type == "@@KEY" and action.payload.name == "ENTER":
return {**state, "submitted": True}
return state
welcome = FlowScreen("welcome", "welcome.kida", welcome_reducer)
Chaining screens
Use>>to create a flow:
from milo import App
from milo.flow import FlowScreen
welcome = FlowScreen("welcome", "welcome.kida", welcome_reducer)
config = FlowScreen("config", "config.kida", config_reducer)
confirm = FlowScreen("confirm", "confirm.kida", confirm_reducer)
flow = welcome >> config >> confirm
app = App.from_flow(flow)
app.run()
Note
The>> operator connects screens in sequence. When a screen sets submitted: True, the flow advances to the next screen by dispatching @@NAVIGATE.
Flow state
At runtime, Milo manages aFlowStatethat isolates each screen's state:
FlowState(
current_screen="welcome",
screen_states={
"welcome": {...},
"config": {...},
"confirm": {...},
},
)
Each screen's reducer only sees its own slice of state. The flow's combined reducer routes actions to the current screen's reducer and handles @@NAVIGATEtransitions.
How FlowState routing works
When the store dispatches an action:
- The flow reducer checks
current_screen - It passes the action to that screen's reducer with only that screen's state slice
- If the reducer returns
submitted: True, the flow auto-dispatches@@NAVIGATEto the next screen @@NAVIGATEactions updatecurrent_screenand re-render with the new screen's template
Screen states persist across navigation — going back to a previous screen restores its state.
Custom transitions
Add non-sequential transitions withwith_transition:
flow = welcome >> config >> confirm
flow = flow.with_transition("welcome", "confirm", on="@@SKIP_CONFIG")
This lets you skip screens, loop back, or branch based on custom actions.
Tip
Use custom transitions for optional screens, error recovery flows, or branching wizards. The flow state machine handles navigation — your reducers just dispatch the right action type.
Navigation actions
Dispatch@@NAVIGATEexplicitly from a reducer to move between screens:
from milo import Action, ReducerResult
def welcome_reducer(state, action):
if action.type == "@@KEY" and action.payload.char == "s":
return ReducerResult(
{**state, "skipped": True},
sagas=(),
)
return state
Or let the flow auto-advance when state["submitted"] becomes True.