Doing something when the journey ends
What you want to do
Every customer should hear how their order went: that the parcel is on its way, with its tracking number, or, for a gift card, its code, or, when the order couldn't be completed, an apology. You want that message written once the journey has ended, whichever step it ended on and whether it succeeded or failed, without any step having to know it is the last one.
The code
The message is written by a workflow policy: a type deriving Default, with an impl block marked #[workflow_policy(name = "notify")]. Its hook marked #[on_workflow_success] asks for the order, its tracking number and its gift card's code from the data bag, and contributes the customer's message under message. Its hook marked #[on_workflow_failure] asks only for the order, and contributes the apology under the same key. A workflow hook decides nothing, so each returns Ok(()).
/// Writes the message that tells the customer how their order went, once its journey has
/// ended.
#[derive(Default)]
struct Notify;
#[workflow_policy(name = "notify")]
impl Notify {
#[on_workflow_success]
fn succeeded(
&self,
#[from_workflow] order: String,
#[from_workflow] tracking: Option<String>,
#[from_workflow] code: Option<String>,
#[contributor] mut contributor: Contributor<'_>,
) -> Result<(), Error> {
let message = match (tracking, code) {
(Some(tracking), _) => format!("Order {order} is on its way, tracking {tracking}."),
(None, Some(code)) => {
format!("Order {order} is paid: your gift card's code is {code}.")
}
(None, None) => format!("Order {order} is complete."),
};
contributor.contribute("message", message);
Ok(())
}
#[on_workflow_failure]
fn failed(
&self,
#[from_workflow] order: String,
#[contributor] mut contributor: Contributor<'_>,
) -> Result<(), Error> {
contributor.contribute(
"message",
format!("Sorry, order {order} could not be completed."),
);
Ok(())
}
}The workflow lists it on its own line, #[workflow(...)], with policies = [Notify], rather than on a step's:
/// The shop's workflow: it charges, repairing a charge that fails and auditing one that succeeds,
/// then ships, and shipping may be tried once more. Once a journey has ended, the customer's
/// message is written.
#[workflow(name = "orders", reporters = [Log, Desk], policies = [Notify])]
#[with_step(Charge, name = "charge", rescue = RenewCard, policies = [Audit, Outage])]
#[with_step(Ship, name = "ship", retry_budget = 1, policies = [HeavyParcels])]
struct Orders;The charge and the audit are chapter 15's, the rescue step RenewCard is chapter 14's and the desk is chapter 11's.
The log is chapter 17's, and now also names a workflow hook by its policy and its hook, since it is about no step, and names the key of a request that found nothing:
EventBody::HookCalled {
hook: HookSource::Workflow { policy, hook, .. },
..
} => Some(format!("{policy}: {hook}")),
EventBody::OptionalInputAbsent { key, .. } => Some(key.clone()),The output now holds the message:
/// What the shop reads back once the journey has ended.
#[derive(Output)]
struct Order {
receipt: Option<String>,
tracking: Option<String>,
audit: Option<String>,
message: Option<String>,
}The rest is chapter 17's shop, which now also reads back the message and prints it. It runs Ann's, Gil's, Jo's and Kim's orders again, then a new customer's: Lee's card expired in 2019, and the faked bank now has no replacement for a card that expired more than three years ago. It prints:
Ann's order:
log orders A-1 #1 journey_started card, gift_card, order, price, weight
log orders A-1 #2 attempt_started charge, attempt 1
log orders A-1 #3 service_provided charge gets payments from bank, a ch18_doing_something_when_the_journey_ends::Bank
the bank charges 40 for order A-1
log orders A-1 #4 step_succeeded charge
log orders A-1 #5 contribution_committed receipt
log orders A-1 #6 hook_called audit: on step success of charge
log orders A-1 #7 contribution_committed audit
log orders A-1 #8 attempt_started ship, attempt 1
log orders A-1 #9 service_provided ship gets carrier from carriers, a ch18_doing_something_when_the_journey_ends::MainCarrier
the main carrier books order A-1
log orders A-1 #10 step_info ship: the parcel leaves with receipt R-A-1
log orders A-1 #11 step_succeeded ship
log orders A-1 #12 contribution_committed tracking
log orders A-1 #13 optional_input_absent code
log orders A-1 #14 hook_called notify: on workflow success
log orders A-1 #15 contribution_committed message
log orders A-1 #16 journey_succeeded
the journey succeeded, with receipt R-A-1 and tracking T-A-1
audit: charge took 40, receipt R-A-1
message: Order A-1 is on its way, tracking T-A-1.
Gil's order:
log orders A-7 #1 journey_started card, gift_card, order, price, weight
log orders A-7 #2 attempt_started charge, attempt 1
log orders A-7 #3 service_provided charge gets payments from bank, a ch18_doing_something_when_the_journey_ends::Bank
the bank charges 45 for order A-7
log orders A-7 #4 step_succeeded charge
log orders A-7 #5 contribution_committed receipt
log orders A-7 #6 hook_called audit: on step success of charge
log orders A-7 #7 contribution_committed audit
log orders A-7 #8 attempt_started ship, attempt 1
log orders A-7 #9 service_provided ship gets carrier from carriers, a ch18_doing_something_when_the_journey_ends::MainCarrier
the main carrier books order A-7
log orders A-7 #10 step_failed ship: too-heavy
log orders A-7 #11 hook_called heavy_parcels: on step retry of ship
log orders A-7 #12 step_retrying ship
log orders A-7 #13 attempt_started ship, attempt 2
log orders A-7 #14 service_provided ship gets carrier from carriers, a ch18_doing_something_when_the_journey_ends::BackupCarrier
the backup carrier books order A-7
log orders A-7 #15 step_info ship: the parcel leaves with receipt R-A-7
log orders A-7 #16 step_succeeded ship
log orders A-7 #17 contribution_committed tracking
log orders A-7 #18 optional_input_absent code
log orders A-7 #19 hook_called notify: on workflow success
log orders A-7 #20 contribution_committed message
log orders A-7 #21 journey_succeeded
the journey succeeded, with receipt R-A-7 and tracking B-A-7
audit: charge took 45, receipt R-A-7
message: Order A-7 is on its way, tracking B-A-7.
Jo's order:
log orders A-8 #1 journey_started card, gift_card, order, price, weight
log orders A-8 #2 attempt_started charge, attempt 1
log orders A-8 #3 service_provided charge gets payments from bank, a ch18_doing_something_when_the_journey_ends::Bank
the bank declines card jo-2024: it has expired
log orders A-8 #4 step_failed charge: card-expired
log orders A-8 #5 step_rescuing charge: failure, rescued by charge_rescue
log orders A-8 #6 attempt_started charge_rescue, attempt 1
the bank says card jo-2024 was replaced by jo-2029
log orders A-8 #7 step_succeeded charge_rescue
log orders A-8 #8 contribution_committed card
log orders A-8 #9 data_overwritten card
log orders A-8 #10 step_rescued charge by charge_rescue
log orders A-8 #11 attempt_started charge, attempt 2
the bank charges 30 for order A-8
log orders A-8 #12 step_succeeded charge
log orders A-8 #13 contribution_committed receipt
log orders A-8 #14 hook_called audit: on step success of charge
log orders A-8 #15 contribution_committed audit
log orders A-8 #16 attempt_started ship, attempt 1
log orders A-8 #17 service_provided ship gets carrier from carriers, a ch18_doing_something_when_the_journey_ends::MainCarrier
the main carrier books order A-8
log orders A-8 #18 step_info ship: the parcel leaves with receipt R-A-8
log orders A-8 #19 step_succeeded ship
log orders A-8 #20 contribution_committed tracking
log orders A-8 #21 optional_input_absent code
log orders A-8 #22 hook_called notify: on workflow success
log orders A-8 #23 contribution_committed message
log orders A-8 #24 journey_succeeded
the journey succeeded, with receipt R-A-8 and tracking T-A-8
audit: charge took 30, receipt R-A-8
message: Order A-8 is on its way, tracking T-A-8.
Kim's order:
log orders A-9 #1 journey_started card, gift_card, order, price, weight
log orders A-9 #2 attempt_started charge, attempt 1
log orders A-9 #3 service_provided charge gets payments from bank, a ch18_doing_something_when_the_journey_ends::Bank
the bank charges 25 for order A-9
log orders A-9 #4 step_succeeded charge
log orders A-9 #5 contribution_committed receipt
log orders A-9 #6 contribution_committed code
log orders A-9 #7 hook_called audit: on step success of charge
log orders A-9 #8 contribution_committed audit
log orders A-9 #9 attempt_started ship, attempt 1
log orders A-9 #10 service_provided ship gets carrier from carriers, a ch18_doing_something_when_the_journey_ends::MainCarrier
log orders A-9 #11 step_skipped ship: nothing-to-ship
log orders A-9 #12 contributions_discarded ship
log orders A-9 #13 optional_input_absent tracking
log orders A-9 #14 hook_called notify: on workflow success
log orders A-9 #15 contribution_committed message
log orders A-9 #16 journey_succeeded
the journey succeeded, with receipt R-A-9 and tracking none
audit: charge took 25, receipt R-A-9
message: Order A-9 is paid: your gift card's code is G-A-9.
Lee's order:
log orders A-10 #1 journey_started card, gift_card, order, price, weight
log orders A-10 #2 attempt_started charge, attempt 1
log orders A-10 #3 service_provided charge gets payments from bank, a ch18_doing_something_when_the_journey_ends::Bank
the bank declines card lee-2019: it has expired
log orders A-10 #4 step_failed charge: card-expired
log orders A-10 #5 step_rescuing charge: failure, rescued by charge_rescue
log orders A-10 #6 attempt_started charge_rescue, attempt 1
the bank says nothing replaced card lee-2019
log orders A-10 #7 step_failed charge_rescue: not-replaced
log orders A-10 #8 step_given_up charge_rescue: failure
log orders A-10 #9 hook_called notify: on workflow failure
log orders A-10 #10 contribution_committed message
log orders A-10 #11 journey_failed charge_rescue
desk: order A-10 failed at charge_rescue
the journey failed, with receipt none and tracking none
audit: none
message: Sorry, order A-10 could not be completed.How it works
- A workflow policy acts on the whole journey. A step policy is attached to steps, and its hooks are about one step's outcome. A workflow policy is attached to the workflow, and its hooks are about the journey. What a journey does at its end belongs to a workflow policy, never to the step that happens to run last.
on workflow successruns once, when the journey has succeeded. It runs after the last step, once the journey's fate is decided, and before the event that says so: in every journey that succeeds,hook_called notify: on workflow successcomes beforejourney_succeeded. Kim's journey ends on a skipped shipping, and Jo's needed a rescue, but each succeeds, so the hook runs for each.on workflow failureruns once, when the journey has failed. Lee's charge fails on the expired card, and the rescue step fails too, withnot-replaced, since the bank has no new card: the rescue step is given up, and the journey fails.on workflow failureruns after that decision and beforejourney_failed, andon workflow successnever runs for Lee. Every journey that succeeds or fails runs exactly one of the two hooks, so each of these customers hears back.- A workflow hook reads the data bag, and nothing about any step. A workflow hook asks for data with
#[from_workflow], by the parameter's name.Option<String>makes a request optional: on success, Kim's order has no tracking number, and Ann's, Gil's and Jo's have no code, so each getsNonefor what its journey never contributed, and the executor records that asoptional_input_absent, naming the key, just before the hook is called. - A workflow hook decides nothing. By the time it runs, the journey's fate is decided, so the hook can only do something about it. That is why each hook's
Okholds(). - A workflow hook's contributions are part of the result. The message is committed when the hook returns, before the journey ends, so the shop reads it back with the rest of the output, Lee's apology included, though that journey failed.
- A workflow policy is built once for each journey. One
Notifyserves the whole journey, made withDefaultbefore it starts, and is dropped when it ends.
Each customer's message now exists, but only in the data bag, and only main prints it. Sending it is something else: today the shop sends it by email, and the workflow policy shouldn't need to know how.
Outcomes
We have shown how a workflow policy does something once a journey has ended, whichever step ended it, and whether it succeeded or failed.
| The usual way | With Itinera |
|---|---|
| A message at the end of a process is often sent by whichever function happens to finish it, so every path that can end the process, failures included, has to remember to send it. | One of the workflow policy's two hooks runs once the journey has ended, whether it succeeded or failed, and no step knows about it. |