Multi-Screen Flows

Declarative multi-screen state machines with the >> operator.

2 min read 406 words

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.

flowchart LR W[Welcome] -->|"@@NAVIGATE"| C[Config] C -->|"@@NAVIGATE"| I[Install] W -.->|"@@SKIP"| I

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.

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.

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.