Creating workflows with multiple state transitions

What are the patterns we can use for creating workflows with multiple state transitions?

I found one pattern in the nextjs-ecommerce-oneclick example where a local variable called purchaseState is used to maintain the state of the workflow and is transitioned to new states when signals are received. Also a wf.condition is used to terminate the workflow:

type PurchaseState = 'PURCHASE_PENDING' | 'PURCHASE_CONFIRMED' | 'PURCHASE_CANCELED';

export const cancelPurchase = wf.defineSignal('cancelPurchase');
export const purchaseStateQuery = wf.defineQuery<PurchaseState>('purchaseState');

export async function OneClickBuy(itemId: string) {
  const itemToBuy = itemId;
  let purchaseState: PurchaseState = 'PURCHASE_PENDING';
  wf.setHandler(cancelPurchase, () => void (purchaseState = 'PURCHASE_CANCELED'));
  wf.setHandler(purchaseStateQuery, () => purchaseState);
  if (await wf.condition(() => purchaseState === 'PURCHASE_CANCELED', '5s')) {
    return await canceledPurchase(itemToBuy);
  } else {
    purchaseState = 'PURCHASE_CONFIRMED';
    return await checkoutItem(itemToBuy);
  }
}

However, this is a simple workflow with only two states and one transition. How do I extend this pattern to multiple transitions? Taking an uber workflow example (and this is just the happy path),

 received ride request
 -> send ride options
 -> received selected option
 -> send ride details
 -> start ride
 -> end ride
 -> send receipt

My initial thought was to use if... else if... else if..., but that seems quite cumbersome. Something like a switch would have been nice, but I feel like I am missing something. What are the common patterns for this scenario? Any examples?

Thinking more, to chain more transitions, one should not return from the workflow until it is fully done. So in the e-commerce example above, extending it to one more transition could be done like this:

type PurchaseState = 'PURCHASE_PENDING' | 'PURCHASE_CONFIRMED' | 'CHECKOUT_COMPLETED' | 'PURCHASE_CANCELED';

export const cancelPurchase = wf.defineSignal('cancelPurchase');
export const checkout = wf.defineSignal('checkout');
export const purchaseStateQuery = wf.defineQuery<PurchaseState>('purchaseState');

export async function OneClickBuy(itemId: string) {
  const itemToBuy = itemId;
  let purchaseState: PurchaseState = 'PURCHASE_PENDING';
  wf.setHandler(cancelPurchase, () => void (purchaseState = 'PURCHASE_CANCELED'));
  wf.setHandler(checkout, () => void (purchaseState = 'CHECKOUT_COMPLETED'));
  wf.setHandler(purchaseStateQuery, () => purchaseState);

  if (await wf.condition(() => purchaseState === 'PURCHASE_CANCELED', '5s')) {
    return await canceledPurchase(itemToBuy);
  } else {
    purchaseState = 'PURCHASE_CONFIRMED';
    // ----- DON'T RETURN HERE -----
    await checkoutItem(itemToBuy);
  }

  if (await wf.condition(() => purchaseState === 'CHECKOUT_COMPLETED', '5s')) {
    return await sendInvoice(itemToBuy);
  } else {
    purchaseState = 'PURCHASE_CANCELED';
    return await canceledPurchase(itemToBuy);
  }
}

Does this work? Any other better approach?

Updating the state variables from signals and using the condition to block workflow progress according to its state works well in many cases.

1 Like

Thank you for confirming @maxim.