Composition
Real systems are not single machines. They are compositions: one machine calling another, each with its own contract, governance, and audit trail. In mashin, composition is a first-class concept. You use ask ... from to call another machine the same way you call a stdlib effect machine. The called machine runs under its own governance rules, and the boundary between caller and callee is clean.
Calling another machine
ask validate, from: "@myorg/orders/validate" order_id: input.order_id returns valid as boolean errors as list assuming valid: true errors: []The from parameter takes a machine path. Three namespaces:
| Namespace | Example | Meaning |
|---|---|---|
@mashin/actions/* | @mashin/actions/http/get | Official standard library |
@orgname/* | @myorg/billing/charge | Organization machines |
| bare name | "data_enricher" | Local machine in the same project |
A composed pipeline
Here is an order processing pipeline built from three machines:
machine process_order
accepts order_id as text, is required customer_id as text, is required
responds with status as text shipping_estimate as number
ensures permissions allowed to machine.call
implements ask validate, from: "@myorg/orders/validate" order_id: input.order_id returns valid as boolean errors as list assuming valid: true errors: []
decide check_validation when steps.validate.valid ask ship, from: "@myorg/shipping/calculate" order_id: input.order_id customer_id: input.customer_id returns cost as number days as number assuming cost: 9.99 days: 3 otherwise compute reject {status: "rejected", shipping_estimate: 0}
compute result { status: "approved", shipping_estimate: steps.ship ? steps.ship.days : 0 }Each ask ... from call invokes an independent machine. @myorg/orders/validate has its own accepts, ensures, and implements. When it runs, its governance rules apply. The caller’s trust level constrains what the callee can do (trust ceiling), but governance does not leak across boundaries.
The encapsulation principle
When a machine grows past 5-6 steps, look for groups of related steps that can be extracted into submachines. The parent should read as a high-level narrative:
// Before: 8 steps in one machineimplements ask fetch_tickets, from: "@mashin/actions/http/get" url: input.api_url compute parse_tickets {parsed: steps.fetch_tickets.body} ask classify, using: "anthropic:claude-haiku-4-5" with task "Classify these tickets" compute filter_urgent {urgent: steps.classify.items.filter(i => i.priority == "urgent")} ask assign_agents, from: "agent_lookup" tickets: filter_urgent.urgent compute match_assignments {matched: steps.assign_agents.assignments} ask notify_slack, from: "@mashin/actions/http/post" url: "https://slack.com/api/notify" compute done {status: "complete"}// After: parent reads as a storyimplements ask get_and_classify, from: "@myorg/support/classify_tickets" api_url: input.api_url
ask assign, from: "@myorg/support/assign_agents" tickets: steps.get_and_classify.urgent
ask notify, from: "@myorg/support/notify_team" assignments: steps.assign.assignmentsEach submachine is independently testable and potentially reusable.
Governance across boundaries
When machine A calls machine B:
- Machine A needs
machine.callpermission (or the specific capability required by B) - Machine B runs under its own governance rules
- A’s trust level is the ceiling for B (B cannot exceed A’s privileges)
- Each machine’s behavioral ledger is independent
- The call itself is recorded in A’s ledger (target, inputs, result)
This is transitive governance. You do not need to redeclare B’s permissions inside A. Each machine is self-governing.
Passing data
Input keys in ask ... from are passed as the called machine’s accepts fields:
// Callerask enrich, from: "customer_enricher" customer_id: input.id include_history: true
// Called machinemachine customer_enricher accepts customer_id as text, is required include_history as boolean, default: falseTesting composed machines
Use assuming to mock the called machine’s response in tests:
ask validate, from: "@myorg/orders/validate" order_id: input.order_id assuming valid: true errors: []In test mode, the called machine is never invoked. The assuming values are returned immediately. This means you can test the caller’s logic without running (or even having access to) the callee.
Try it
Build a three-machine pipeline: one machine that fetches data, one that classifies it with an LLM, and a parent machine that orchestrates them. Use ask ... from in the parent to call the other two. Add assuming blocks so the parent can be tested independently.
Next steps
- Flows - Multi-flow machines for complex execution paths
- Effects - Stdlib effect machines
- ask … from reference - Full specification