Skip to content

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 step
classify.confidence -- reads the confidence score
input.sender -- reads the original input

Data 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_alert in 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_message

The 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.

Next: Going Live →