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
Section titled “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
behaves 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 when classify.confidence < 0.7 run flag_for_human when classify.priority == "urgent" run notify_team when classify.priority == "today" run create_task otherwise {routed_to: "archive", action_taken: "none"}
flows flow notify_team ask send_alert, from: "@mashin/actions/microsoft/graph/send_teams_message" team_id: "ops-team" channel_id: "ops-alerts" message_body: "Urgent: " + input.subject + " from " + input.sender compute done {routed_to: "notify_team", action_taken: "Teams alert sent"}
flow create_task ask make_task, from: "@mashin/actions/microsoft/graph/create_planner_task" plan_id: "email-triage" title: input.subject description: classify.reason compute done {routed_to: "create_task", action_taken: "Planner task created"}
flow flag_for_human ask flag, from: "@mashin/actions/microsoft/graph/send_teams_message" team_id: "ops-team" channel_id: "email-review" message_body: "Review needed: " + input.subject compute done {routed_to: "human_review", action_taken: "Flagged for review"}
verifies test "routes urgent email to the team" assuming classify {priority: "urgent", confidence: 0.95, reason: "Outage"} assuming send_alert {message_id: "1629454149", created_at: "2026-04-01T10:30:45Z", success: true, status_code: 201} expect {routed_to: "notify_team"} test "archives a low-priority email" assuming classify {priority: "later", confidence: 0.9, reason: "FYI"} expect {routed_to: "archive"}When there is only one path, steps go directly under behaves. When there are multiple paths, use flows with named flow blocks.
How Steps Share Data
Section titled “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
Section titled “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
behaves ask notify, from: "@mashin/actions/microsoft/graph/send_teams_message" team_id: "ops-team" channel_id: "ops-alerts" message_body: "Urgent: " + input.subject + " from " + input.sender + "\n" + input.reason compute done {sent: notify.success}
verifies test "sends the alert and reports success" assuming notify {message_id: "1629454149", created_at: "2026-04-01T10:30:45Z", success: true, status_code: 201} expect {sent: true} test "reports a rejected alert as not sent" assuming notify {success: false, status_code: 403} expect {sent: false}{sent: notify.success} passes the real answer through instead of hardcoding true. The second test is the one that matters: when Teams refuses the message with a 403, this machine says so.
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 action machine. The interface is identical. Your machine and the standard library machines work the same way.
Why Composition Matters
Section titled “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
Section titled “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/graph/send_teams_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
Section titled “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.