This tutorial walks you through building a multi-screen install wizard — the kind of interactive CLI you'd use for setting up a tool, configuring a service, or onboarding a user.
What you'll build
A three-screen wizard:
- Welcome — greet the user, explain what happens next
- Config — collect a project name, environment, and confirmation via form fields
- Install — run the install saga, show a progress bar, display results
Prerequisites
- Milo installed
- Python 3.14+
- Read the Quickstart
Build the Wizard
- 1
Define the screens
Create three FlowScreens with templates and reducers
Each screen is a
FlowScreenwith its own template and reducer:from milo.flow import FlowScreen welcome = FlowScreen("welcome", "welcome.kida", welcome_reducer) config = FlowScreen("config", "config.kida", config_reducer) install = FlowScreen("install", "install.kida", install_reducer) - 2
Write the welcome reducer
Advance on Enter
The welcome screen advances to config when the user presses Enter:
def welcome_reducer(state, action): if state is None: return {"ready": False} if action.type == "@@KEY" and action.payload.name == "ENTER": return {**state, "ready": True, "submitted": True} return state - 3
Write the config reducer
Use form_reducer for structured input
The config screen uses
form_reducerto handle form fields:from milo import FieldSpec, FieldType from milo.form import form_reducer config_specs = [ FieldSpec("project", "Project name"), FieldSpec("env", "Environment", field_type=FieldType.SELECT, choices=("dev", "staging", "prod")), FieldSpec("confirm", "Proceed with install?", field_type=FieldType.CONFIRM), ] def config_reducer(state, action): if state is None: return form_reducer({"specs": config_specs}, action) return form_reducer(state, action) - 4
Write the install reducer and saga
Trigger side effects with ReducerResult
The install screen triggers a saga on entry and tracks progress:
from milo import ReducerResult, Call, Put, Delay, Action def install_reducer(state, action): if state is None: return ReducerResult( {"progress": 0, "status": "installing", "log": []}, sagas=(install_saga,), ) if action.type == "PROGRESS": return {**state, "progress": action.payload} if action.type == "INSTALL_DONE": return {**state, "progress": 100, "status": "done", "log": action.payload} return state def install_saga(): for i in range(1, 11): yield Delay(0.3) yield Put(Action("PROGRESS", payload=i * 10)) result = yield Call(run_install, ()) yield Put(Action("INSTALL_DONE", payload=result)) - 5
Create the templates
Kida templates for each screen
{{ "Install Wizard" | bold }} {{ "=" * 40 | fg("dim") }} This wizard will set up your project. Press ENTER to continue.{% include "form.kida" %}Uses the built-in form template.
{{ "Installing..." | bold }} {% include "progress.kida" %} {% for line in log %} {{ line }} {% end %} {% if status == "done" %} {{ "Done!" | fg("green") | bold }} {% end %} - 6
Wire it up
Chain screens and run
from milo import App flow = welcome >> config >> install app = App.from_flow(flow) app.run()
Next steps
Ideas for extending this wizard
- Add validation to the config form with
FieldSpec.validator - Record the session with
record=Truefor replay testing - Add a fourth "summary" screen that shows what was configured
- Add a custom transition to skip config:
flow.with_transition("welcome", "install", on="@@QUICK_INSTALL") - Handle install errors in the saga and dispatch an error action