Lesson 5: Composition
Your email triage machine has grown. Classification, routing, Teams notifications, Planner tasks. The steps work, but they are starting to pile up in one machine.
This lesson organizes them into flows, shows how steps pass data to each other, and introduces the idea of calling one machine from another.
Flows: Named Groups of Steps
You saw flows briefly in Lesson 4. Here is the full picture:
machine email_triage
accepts subject as text, is required sender as text, is required body as text
responds with routed_to as text action_taken as text
implements ask classify, using: "anthropic:claude-haiku-4" with task "Classify this email.\n\nFrom: ${input.sender}\nSubject: ${input.subject}\nBody: ${input.body}" returns priority as text, is required, choices: ["urgent", "today", "later", "ignore"] confidence as number, is required, range: [0.0, 1.0] reason as text
decide route if classify.confidence < 0.7 run flow(flag_for_human) else if classify.priority == "urgent" run flow(notify_team) else if classify.priority == "today" run flow(create_task) else {routed_to: "archive", action_taken: "none"}
flows flow notify_team ask send_alert, from: "@mashin/actions/microsoft/teams/send_message" channel: "ops-alerts" message: "Urgent: " + input.subject + " from " + input.sender {routed_to: "notify_team", action_taken: "Teams alert sent"}
flow create_task ask make_task, from: "@mashin/actions/microsoft/planner/create_task" title: input.subject description: classify.reason {routed_to: "create_task", action_taken: "Planner task created"}
flow flag_for_human ask flag, from: "@mashin/actions/microsoft/teams/send_message" channel: "email-review" message: "Review needed: " + input.subject {routed_to: "human_review", action_taken: "Flagged for review"}When there is only one path, steps go directly under implements. When there are multiple paths, use flows with named flow blocks.
How Steps Share Data
Every step can read the output of any previous step by name:
classify.priority -- reads the priority from the classify stepclassify.confidence -- reads the confidence scoreinput.sender -- reads the original inputData flows forward. Each step can see everything that came before it. No explicit wiring, no variable passing. Just stepname.fieldname.
Calling Other Machines
As machines grow, split them up. Your notification logic can be its own machine:
machine send_urgent_alert
accepts subject as text, is required sender as text, is required reason as text
responds with sent as boolean
implements ask notify, from: "@mashin/actions/microsoft/teams/send_message" channel: "ops-alerts" message: "Urgent: " + input.subject + " from " + input.sender + "\n" + input.reason {sent: true}Then call it from your triage machine:
flow notify_team ask alert, from: send_urgent_alert subject: input.subject sender: input.sender reason: classify.reason {routed_to: "notify_team", action_taken: "Alert sent"}ask alert, from: send_urgent_alert calls another machine the same way you call a built-in effect machine. The interface is identical. Your machine and the standard library machines work the same way.
Why Composition Matters
Small machines are:
- Testable: Test the notification logic separately from the classification logic
- Reusable: Use
send_urgent_alertin other machines, not just email triage - Replaceable: Swap Teams for Slack by changing one machine, not editing everywhere it is used
- Readable: Each machine does one thing. The name tells you what.
When a machine has more than 5-6 steps, look for groups that can be extracted. The parent machine should read like a summary: classify, route, act.
The Trace Shows Everything
/timeline [run_id][1] classify ask 420ms $0.0003 claude-haiku-4[2] route decide 0ms $0.0000[3] alert ask 850ms $0.0000 send_urgent_alert [3.1] notify ask 840ms $0.0000 @mashin/actions/microsoft/teams/send_messageThe trace is hierarchical. Step 3 called send_urgent_alert, which internally called the Teams API. You see the full call chain, every machine involved, every cost.
What Comes Next
You have a working, well-organized email triage system. Next lesson: putting it in production. Deploy it as an endpoint that runs automatically, schedule it to check email every 5 minutes, and monitor what it does.