Skip to content

Quick Start

This guide builds a realistic flow and explains not only the code, but also why each part exists.

Scenario

We will model a simple payment workflow:

  1. reserve inventory
  2. process payment
  3. send a confirmation email

If payment fails, inventory must be released. That is exactly the kind of situation where Orchestrix is useful: multiple steps, side effects, and a clear rollback path.

1. Define the flow

ts
import { create } from "@eddiecbrl/orchestrix";

type PaymentInput = {
  orderId: string;
  amount: number;
  userId: string;
};

const paymentFlow = create<PaymentInput>("payment-flow")
  .step("reserve-stock", async (ctx) => {
    console.log(`Reserving stock for order ${ctx.input.orderId}`);
  }, {
    compensate: async (ctx) => {
      console.log(`Releasing stock for order ${ctx.input.orderId}`);
    }
  })
  .step("process-payment", async (ctx) => {
    console.log(`Processing payment of $${ctx.input.amount} for user ${ctx.input.userId}`);

    if (ctx.input.amount > 1000) {
      throw new Error("Payment declined: limit exceeded");
    }
  }, {
    retries: 3,
    retryDelayMs: 1000
  })
  .step("send-email", async (ctx) => {
    console.log(`Sending confirmation email to user ${ctx.input.userId}`);
  });

Why this flow is structured this way

reserve-stock

This step has a compensation function because it creates a side effect that may need to be undone later.

process-payment

This step uses retries because payment providers and external services may fail temporarily. Retries belong on the step that talks to the unstable dependency.

send-email

This step does not need compensation in this example because a confirmation email is not usually something you "undo". Whether a step should compensate depends on business semantics, not just technical capability.

2. Run the flow

ts
const successResult = await paymentFlow.run({
  orderId: "order_1",
  amount: 50,
  userId: "user_A"
});

console.log(successResult.status); // "completed"

const failResult = await paymentFlow.run({
  orderId: "order_2",
  amount: 2000,
  userId: "user_B"
});

console.log(failResult.status); // "failed"

What happens in the success case

For order_1, the flow:

  1. reserves stock
  2. processes payment
  3. sends the email
  4. returns completed

No compensation runs because nothing failed.

What happens in the failure case

For order_2, the flow:

  1. reserves stock successfully
  2. enters process-payment
  3. retries that step according to its retry configuration
  4. marks the flow as failed after retries are exhausted
  5. runs the compensation for reserve-stock

That last point is the key: Orchestrix compensates previously completed steps, not the step that just failed.

3. Inspect the result

The run() method returns a detailed execution report:

ts
console.log(successResult.durationMs);
console.log(successResult.steps.map((s) => `${s.name}: ${s.status}`));

A typical successful output might look like:

ts
[
  "reserve-stock: completed",
  "process-payment: completed",
  "send-email: completed"
]

For a failed run, you can inspect the failing step:

ts
if (failResult.status === "failed") {
  const failedStep = failResult.steps.find((step) => step.status === "failed");
  console.log(failedStep?.name);
  console.log(failedStep?.attempts);
  console.log(failedStep?.error);
}

What this example teaches

This single flow already covers the main Orchestrix mental model:

  • steps model business actions
  • retries handle temporary failures
  • compensation handles rollback
  • the result object makes execution visible

Common next improvement

In production, this same flow would often also include:

  • an idempotency key to prevent double-charging
  • hooks for logging and metrics
  • a timeout on the payment step

Example:

ts
const result = await paymentFlow.run(input, {
  key: `payment:${input.orderId}`,
  ttlMs: 60 * 60 * 1000
});

What's Next?

Released under the MIT License.