# Build an Install Wizard URL: /docs/tutorials/build-a-wizard/ Section: tutorials Tags: tutorials, flows, forms, sagas -------------------------------------------------------------------------------- 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 flowchart LR W[Welcome] -->|Enter| C[Config Form] C -->|Submit| I[Install] I -->|saga completes| Done[Exit] 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 2/3 complete Milo installed Python 3.14+ Read the Quickstart (function() { const checklist = document.currentScript.closest('.checklist'); if (!checklist) return; const progressBar = checklist.querySelector('.checklist-progress-bar'); const progressText = checklist.querySelector('.checklist-progress-text'); const checkboxes = checklist.querySelectorAll('input[type="checkbox"]'); if (!progressBar || !progressText || !checkboxes.length) return; function updateProgress() { const total = checkboxes.length; const checked = Array.from(checkboxes).filter(cb => cb.checked).length; const percentage = Math.round((checked / total) * 100); progressBar.style.width = percentage + '%'; progressText.textContent = checked + '/' + total; } checkboxes.forEach(function(checkbox) { checkbox.addEventListener('change', updateProgress); }); })(); Build the Wizard 1Define the screensCreate three FlowScreens with templates and reducers Each screen is a FlowScreen with 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) 2Write the welcome reducerAdvance 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 3Write the config reducerUse form_reducer for structured input The config screen uses form_reducer to 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) 4Write the install reducer and sagaTrigger 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)) 5Create the templatesKida templates for each screen welcome.kida config.kida install.kida {{ "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 %} 6Wire it upChain screens and run from milo import App flow = welcome >> config >> install app = App.from_flow(flow) app.run() Next steps Tip Ideas for extending this wizard Add validation to the config form with FieldSpec.validator Record the session with record=True for 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 -------------------------------------------------------------------------------- Metadata: - Author: lbliii - Word Count: 549 - Reading Time: 3 minutes