Using a step in another workflow
What you want to do
Customers can now collect their orders at the shop's counter, paying at the counter's card terminal and taking the goods away, so nothing is shipped. You want the counter to look the order up, price it and charge it exactly as delivered orders are, without copying any of that code, and to decide for itself everything else.
The code
The shop is the one chapter 37 lists. The counter's terminal is one more implementation of the Payments trait the charge already asks for, as chapter 12 shows it with its error:
/// The shop counter's card terminal, faked: it declines a card that expired before this year,
/// and prints what it does. Its receipts start with `C`.
struct Terminal {
this_year: u32,
}
impl Payments for Terminal {
fn charge(&self, order: &str, card: &str, amount: i64) -> Result<Option<String>, NetworkDown> {
if expiry(card).is_some_and(|year| year < self.this_year) {
println!("the counter's terminal declines card {card}: it has expired");
return Ok(None);
}
println!("the counter's terminal charges {amount} for order {order}");
Ok(Some(format!("C-{order}")))
}
}/// The shop counter's card terminal, faked: it declines a card that expired before this year,
/// and prints what it does once it has answered. Its receipts start with `C`.
struct Terminal {
this_year: u32,
}
#[async_trait]
impl Payments for Terminal {
async fn charge(
&self,
order: &str,
card: &str,
amount: i64,
) -> Result<Option<String>, NetworkDown> {
network().await;
if expiry(card).is_some_and(|year| year < self.this_year) {
println!("the counter's terminal declines card {card}: it has expired");
return Ok(None);
}
println!("the counter's terminal charges {amount} for order {order}");
Ok(Some(format!("C-{order}")))
}
}The counter's workflow lists three of the shop's steps, Lookup, Price and Charge, under the same names, and provides the order book and the terminal. Its initial data has what those steps read, and no parcel:
/// The counter's workflow: it looks the order up, and finishes an order already completed.
/// Otherwise it works out what the order costs, then charges it at the counter's terminal and
/// audits the charge. The customer takes the goods away, so nothing is shipped, and a card the
/// terminal declines is handed back rather than repaired.
#[workflow(name = "counter", reporters = [Log])]
#[with_step(Lookup, name = "lookup", policies = [AlreadyCompleted])]
#[with_step(Price, name = "price")]
#[with_step(Charge, name = "charge", policies = [Audit])]
struct Counter;
#[workflow]
impl Counter {
/// Names each journey after its order.
#[id_generator]
fn order_number(&self, data: &DataBag) -> Result<String, Error> {
Ok(data.required("order")?)
}
/// The order book, which has the orders completed before today.
#[provider(key = "order_book")]
fn ledger(&self) -> Result<Provided<dyn OrderBook>, Error> {
Ok(provide!(Ledger {
completed: vec!["A-13"],
}))
}
/// The counter's terminal, as the payment client of the journey.
#[provider(key = "payments")]
fn terminal(&self) -> Result<Provided<dyn Payments>, Error> {
Ok(provide!(Terminal { this_year: 2026 }))
}
}
/// What the shop knows about an order collected at the counter when its journey starts.
#[derive(InitialData)]
struct CounterOrder {
order: String,
price: i64,
discount: Option<i64>,
card: String,
gift_card: bool,
}/// The counter's workflow: it looks the order up, and finishes an order already completed.
/// Otherwise it works out what the order costs, then charges it at the counter's terminal and
/// audits the charge. The customer takes the goods away, so nothing is shipped, and a card the
/// terminal declines is handed back rather than repaired.
#[workflow(name = "counter", async_reporters = [Log])]
#[with_step(Lookup, name = "lookup", policies = [AlreadyCompleted])]
#[with_step(Price, name = "price")]
#[with_step(Charge, name = "charge", policies = [Audit])]
struct Counter;
#[workflow]
impl Counter {
/// Names each journey after its order.
#[id_generator]
fn order_number(&self, data: &DataBag) -> Result<String, Error> {
Ok(data.required("order")?)
}
/// The order book, which has the orders completed before today.
#[provider(key = "order_book")]
fn ledger(&self) -> Result<Provided<dyn OrderBook>, Error> {
Ok(provide!(Ledger {
completed: vec!["A-13"],
}))
}
/// The counter's terminal, as the payment client of the journey.
#[provider(key = "payments")]
fn terminal(&self) -> Result<Provided<dyn Payments>, Error> {
Ok(provide!(Terminal { this_year: 2026 }))
}
}
/// What the shop knows about an order collected at the counter when its journey starts.
#[derive(InitialData)]
struct CounterOrder {
order: String,
price: i64,
discount: Option<i64>,
card: String,
gift_card: bool,
}What it names is as earlier chapters show it: the lookup, already_completed and OrderBook in chapter 22, the pricing in chapter 20, the charge in chapter 33, the audit in chapter 34, and the log in chapter 33, chapter 35 and chapter 36.
main prints the listing of each workflow with entry, as chapter 37 did:
println!("orders:");
for listed in Orders::descriptor().listing() {
println!("{}", entry(&listed));
}
println!("counter:");
for listed in Counter::descriptor().listing() {
println!("{}", entry(&listed));
}Then it runs two new orders at the counter: Nia's, with a discount, and Omar's, whose card has expired:
let mut executor = local_executor_for!(Counter);
// The order, the customer, the price, the discount and the card.
let orders = [
("A-17", "Nia", 18, Some(3), "nia-2028"),
("A-18", "Omar", 12, None, "omar-2023"),
];
for (order, customer, price, discount, card) in orders {
println!("{customer}'s order {order}, at the counter:");
let instance = Counter::builder()
.reporters_global_metadata("customer", customer.to_owned())
.initial_data(CounterOrder {
order: order.to_owned(),
price,
discount,
card: card.to_owned(),
gift_card: false,
})
.create()?;
follow_up(&executor.run(instance)?)?;
}// The order, the customer, the price, the discount and the card.
let orders = [
("A-17", "Nia", 18, Some(3), "nia-2028"),
("A-18", "Omar", 12, None, "omar-2023"),
];
for (order, customer, price, discount, card) in orders {
println!("{customer}'s order {order}, at the counter:");
let instance = Counter::builder()
.reporters_global_metadata("customer", customer.to_owned())
.initial_data(CounterOrder {
order: order.to_owned(),
price,
discount,
card: card.to_owned(),
gift_card: false,
})
.create()?;
follow_up(&journey(instance).await?)?;
}follow_up now reads the counter's output, and says to hand back a card the terminal declined:
/// What the counter reads back once the journey has ended.
#[derive(Output)]
struct Collected {
receipt: Option<String>,
audit: Option<String>,
}
/// Says how a journey at the counter ended.
fn follow_up(result: &JourneyResult) -> Result<(), Box<dyn std::error::Error>> {
let data: Collected = result.output()?;
match &result.status {
JourneyStatus::Succeeded { .. } => {
let receipt = data.receipt.as_deref().unwrap_or("none");
println!("the journey succeeded, with receipt {receipt}");
println!("audit: {}", data.audit.as_deref().unwrap_or("none"));
}
JourneyStatus::Failed {
failure: Failure::Failure(reason),
..
} => println!("the journey failed: {}, hand the card back", reason.code()),
_ => println!("the journey ended as {}", result.status.kind()),
}
Ok(())
}/// What the counter reads back once the journey has ended.
#[derive(Output)]
struct Collected {
receipt: Option<String>,
audit: Option<String>,
}
/// Says how a journey at the counter ended.
fn follow_up(result: &JourneyResult) -> Result<(), Box<dyn std::error::Error>> {
let data: Collected = result.output()?;
match &result.status {
JourneyStatus::Succeeded { .. } => {
let receipt = data.receipt.as_deref().unwrap_or("none");
println!("the journey succeeded, with receipt {receipt}");
println!("audit: {}", data.audit.as_deref().unwrap_or("none"));
}
JourneyStatus::Failed {
failure: Failure::Failure(reason),
..
} => println!("the journey failed: {}, hand the card back", reason.code()),
_ => println!("the journey ended as {}", result.status.kind()),
}
Ok(())
}
/// Runs a journey at the counter with an executor of its own.
async fn journey(instance: Instance<Counter, Asynchronous>) -> Result<JourneyResult, Refusal> {
local_executor_for!(Counter).run(instance).await
}In the asynchronous form, journey runs a counter's instance with an executor of its own, as chapter 26's does for the shop, Payments and the terminal take #[async_trait], the counter lists Log under async_reporters, and each journey is awaited. It prints:
orders:
1. lookup, with already_completed; services: order_book from ledger
2. price
3. charge, with audit, outage; services: payments from bank; rescued by charge_rescue, services: cards from cards
4. ship, with failed_shipping; adapted by parcels; services: carrier from carriers
counter:
1. lookup, with already_completed; services: order_book from ledger
2. price
3. charge, with audit; services: payments from terminal
Nia's order A-17, at the counter:
log counter A-17 for Nia #1 journey_started card, discount, gift_card, order, price
log counter A-17 for Nia #2 attempt_started lookup, attempt 1
log counter A-17 for Nia #3 service_provided lookup gets order_book from ledger, a ch42_using_a_step_in_another_workflow::Ledger
the order book has no order A-17 yet
log counter A-17 for Nia #4 step_succeeded lookup
log counter A-17 for Nia #5 contribution_committed completed
log counter A-17 for Nia #6 hook_called already_completed: on step success of lookup
log counter A-17 for Nia #7 attempt_started price, attempt 1
log counter A-17 for Nia #8 step_succeeded price
log counter A-17 for Nia #9 contribution_committed amount
log counter A-17 for Nia #10 attempt_started charge, attempt 1
log counter A-17 for Nia #11 service_provided charge gets payments from terminal, a ch42_using_a_step_in_another_workflow::Terminal
the counter's terminal charges 15 for order A-17
log counter A-17 for Nia, receipt C-A-17 #12 global_metadata_set receipt
log counter A-17 for Nia, receipt C-A-17 #13 step_succeeded charge
log counter A-17 for Nia, receipt C-A-17 #14 contribution_committed receipt
log counter A-17 for Nia, receipt C-A-17 #15 hook_called audit: on step success of charge
log counter A-17 for Nia, receipt C-A-17 #16 contribution_committed audit
log counter A-17 for Nia, receipt C-A-17 #17 journey_succeeded
the journey succeeded, with receipt C-A-17
audit: A-17: charge took 15 on attempt 1, receipt C-A-17
Omar's order A-18, at the counter:
log counter A-18 for Omar #1 journey_started card, gift_card, order, price
log counter A-18 for Omar #2 attempt_started lookup, attempt 1
log counter A-18 for Omar #3 service_provided lookup gets order_book from ledger, a ch42_using_a_step_in_another_workflow::Ledger
the order book has no order A-18 yet
log counter A-18 for Omar #4 step_succeeded lookup
log counter A-18 for Omar #5 contribution_committed completed
log counter A-18 for Omar #6 hook_called already_completed: on step success of lookup
log counter A-18 for Omar #7 attempt_started price, attempt 1
log counter A-18 for Omar #8 optional_input_absent discount
log counter A-18 for Omar #9 step_succeeded price
log counter A-18 for Omar #10 contribution_committed amount
log counter A-18 for Omar #11 attempt_started charge, attempt 1
log counter A-18 for Omar #12 service_provided charge gets payments from terminal, a ch42_using_a_step_in_another_workflow::Terminal
the counter's terminal declines card omar-2023: it has expired
log counter A-18 for Omar #13 step_failed charge: card-expired
log counter A-18 for Omar #14 step_given_up charge: failure
log counter A-18 for Omar #15 journey_failed charge
the journey failed: card-expired, hand the card backorders:
1. lookup, with already_completed; services: order_book from ledger
2. price
3. charge, with audit, outage; services: payments from bank; rescued by charge_rescue, services: cards from cards
4. ship, with failed_shipping; adapted by parcels; services: carrier from carriers
counter:
1. lookup, with already_completed; services: order_book from ledger
2. price
3. charge, with audit; services: payments from terminal
Nia's order A-17, at the counter:
log counter A-17 for Nia #1 journey_started card, discount, gift_card, order, price
log counter A-17 for Nia #2 attempt_started lookup, attempt 1
log counter A-17 for Nia #3 service_provided lookup gets order_book from ledger, a ch42_using_a_step_in_another_workflow_async::Ledger
the order book has no order A-17 yet
log counter A-17 for Nia #4 step_succeeded lookup
log counter A-17 for Nia #5 contribution_committed completed
log counter A-17 for Nia #6 hook_called already_completed: on step success of lookup
log counter A-17 for Nia #7 attempt_started price, attempt 1
log counter A-17 for Nia #8 step_succeeded price
log counter A-17 for Nia #9 contribution_committed amount
log counter A-17 for Nia #10 attempt_started charge, attempt 1
log counter A-17 for Nia #11 service_provided charge gets payments from terminal, a ch42_using_a_step_in_another_workflow_async::Terminal
the counter's terminal charges 15 for order A-17
log counter A-17 for Nia, receipt C-A-17 #12 global_metadata_set receipt
log counter A-17 for Nia, receipt C-A-17 #13 step_succeeded charge
log counter A-17 for Nia, receipt C-A-17 #14 contribution_committed receipt
log counter A-17 for Nia, receipt C-A-17 #15 hook_called audit: on step success of charge
log counter A-17 for Nia, receipt C-A-17 #16 contribution_committed audit
log counter A-17 for Nia, receipt C-A-17 #17 journey_succeeded
the journey succeeded, with receipt C-A-17
audit: A-17: charge took 15 on attempt 1, receipt C-A-17
Omar's order A-18, at the counter:
log counter A-18 for Omar #1 journey_started card, gift_card, order, price
log counter A-18 for Omar #2 attempt_started lookup, attempt 1
log counter A-18 for Omar #3 service_provided lookup gets order_book from ledger, a ch42_using_a_step_in_another_workflow_async::Ledger
the order book has no order A-18 yet
log counter A-18 for Omar #4 step_succeeded lookup
log counter A-18 for Omar #5 contribution_committed completed
log counter A-18 for Omar #6 hook_called already_completed: on step success of lookup
log counter A-18 for Omar #7 attempt_started price, attempt 1
log counter A-18 for Omar #8 optional_input_absent discount
log counter A-18 for Omar #9 step_succeeded price
log counter A-18 for Omar #10 contribution_committed amount
log counter A-18 for Omar #11 attempt_started charge, attempt 1
log counter A-18 for Omar #12 service_provided charge gets payments from terminal, a ch42_using_a_step_in_another_workflow_async::Terminal
the counter's terminal declines card omar-2023: it has expired
log counter A-18 for Omar #13 step_failed charge: card-expired
log counter A-18 for Omar #14 step_given_up charge: failure
log counter A-18 for Omar #15 journey_failed charge
the journey failed: card-expired, hand the card backHow it works
- A step belongs to no workflow.
Lookup,PriceandChargesay what they read and which services they ask for, by key, and how their work went. Nothing in them namesorders, the next step or a policy, so the counter lists them as they are, and they run as they do for a delivered order: Nia's price is 18 less 3, and the charge takes 15. - Each workflow provides its own services. The charge asks for
payments, and gets the bank inordersand the terminal incounter: the listings say so, and so doesservice_provided, event 11. The counter's receipts start withC, and the charge never knew which client it had. - Each workflow decides its own flow. In
orders, a charge that fails because the card has expired is rescued, as chapter 14 showed. Incounter, it has no rescue step, so the same failure,card-expired, gives the charge up and fails Omar's journey: the customer is standing there, and can hand over another card.counterhas noship, so none of its retry budget or policies, and nooutagepolicy, since the counter's terminal has no network to lose. - Each workflow decides its own policies and reporters.
AuditandAlreadyCompletedare attached in both, andAuditwrites the same audit line.counterlists theLogreporter alone, and no workflow policy, so nobody is told by email: the log's lines start with the workflow's name,counter. - Both workflows live in one program. Each is declared once and checked as it compiles, as chapter 37 showed: had
counterlistedChargewithout providingpayments, it would not compile, with the violationservice not provided.
A step can now be changed once for every workflow that lists it, which is how the shop wants its coding agents to work: they change its business rules, often in steps. The shop wants to know that such a change leaves every order's flow as a person agreed.
Outcomes
We have shown how one step runs in two workflows, each deciding its own services, flow, policies and reporters.
| The usual way | With Itinera |
|---|---|
| A second sales channel often copies the first one's code, or adds flags to it for the cases that differ. | The second workflow lists the same steps, and declares only what differs. |
| A function reused by a second channel often has to learn which channel called it. | The charge runs unchanged in both workflows, with whichever client each provides. |