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:
- reserve inventory
- process payment
- 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
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
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:
- reserves stock
- processes payment
- sends the email
- returns
completed
No compensation runs because nothing failed.
What happens in the failure case
For order_2, the flow:
- reserves stock successfully
- enters
process-payment - retries that step according to its retry configuration
- marks the flow as failed after retries are exhausted
- 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:
console.log(successResult.durationMs);
console.log(successResult.steps.map((s) => `${s.name}: ${s.status}`));A typical successful output might look like:
[
"reserve-stock: completed",
"process-payment: completed",
"send-email: completed"
]For a failed run, you can inspect the failing step:
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:
const result = await paymentFlow.run(input, {
key: `payment:${input.orderId}`,
ttlMs: 60 * 60 * 1000
});What's Next?
- Read the Saga Pattern guide to go deeper on rollback behavior.
- Understand Retries before tuning retry policies.
- Understand Idempotency before using the flow in production.