Flows
A Flow is the core unit of orchestration in Orchestrix.
It represents a complete business process composed of named execution nodes. Those nodes can be regular sequential steps or parallel step groups.
How to think about a flow
A useful mental model is:
- a flow is the orchestration boundary
- steps are the business actions inside that boundary
- the flow context is the shared memory for that execution
- the flow result is the execution report you get back
You should create one flow per meaningful workflow, not one flow per single function call.
Defining a flow
You create a flow using the create function:
import { create } from "@eddiecbrl/orchestrix";
const myFlow = create("my-awesome-flow");The name is important because it appears in results, hooks, logs, and debugging output.
Type safety
Orchestrix is TypeScript-first. You can type the input so every step receives a strongly typed ctx.input.
type MyInput = {
id: string;
count: number;
};
const myFlow = create<MyInput>("typed-flow");This is one of the easiest ways to make larger workflows easier to maintain over time.
Configuration
When creating a flow, you can provide global configuration options:
const myFlow = create("configured-flow", {
schema: myZodSchema,
idempotency: myStore,
logging: true,
plugins: [myPlugin],
});What each config is for
schema: validates input before any step runsidempotency: prevents dangerous duplicate executionslogging: enables the built-in logger pluginplugins: adds reusable lifecycle behavior
Not every flow needs all of these. A small internal workflow may only need steps. A payment or webhook flow usually needs idempotency and observability.
Adding steps
Steps are added with .step():
myFlow
.step("step-one", async (ctx) => { /* ... */ })
.step("step-two", async (ctx) => { /* ... */ });You can also add a parallel execution block with .parallel(...) when multiple steps are independent.
Running a flow
Use .run() to execute the flow:
const result = await myFlow.run({ some: "input" });
if (result.status === "completed") {
console.log("Flow finished successfully");
}The return value is always important. Even if you do not expose it externally, it is your main source of truth about what happened during execution.
Execution lifecycle
When run() is called, Orchestrix:
- validates the input if a schema is configured
- checks idempotency if a store is configured and a key is provided
- creates a fresh execution context
- executes steps in order
- applies step-level retries and timeouts
- triggers lifecycle hooks and plugin hooks
- compensates previously completed work if execution fails
- returns a structured
FlowResult
What a flow returns
Every execution returns a FlowResult:
type FlowResult = {
name: string;
status: "pending" | "running" | "completed" | "failed" | "cancelled";
durationMs: number;
steps: StepResult[];
error?: unknown;
};This is what allows Orchestrix to be operationally clear. You do not just get success or failure. You get a report of how execution unfolded.
When to split a flow
If a flow starts feeling confusing, it often means one of these is true:
- a step is doing too much
- unrelated business concerns are mixed together
- long-running waiting behavior should be modeled as multiple flows triggered by events
As a rule of thumb:
- split a step when it hides multiple business decisions
- split a flow when the workflow spans disconnected lifecycle stages
Best Practices
- Use one flow for one business process.
- Give flows stable, descriptive names.
- Keep orchestration logic in the flow and domain logic inside step functions.
- Treat the returned
FlowResultas part of your operational interface.