Skip to main content
The @ooneex/workflow component is a transition-based workflow engine. A workflow is an ordered list of transitions — small, focused steps that each decide whether they run, do their work, and know how to undo it. When you run a workflow, the active transitions execute in order; if any step throws, the engine rolls back the ones that already succeeded, in reverse, and raises a typed WorkflowException.

Why this component

  • Transition-based. Break a process into small, focused steps instead of one monolithic function.
  • Conditional execution. Each transition’s isActive(data, context?) decides whether it runs for the current data.
  • Automatic rollback. When a step fails, the executed transitions roll back in reverse order before the error propagates.
  • Lifecycle hooks. React around every step with onStart, onFinish, and onFail.
  • Container-managed. Register workflows and transitions with a decorator and resolve them from @ooneex/container.
  • Type-safe. Generic Data and Output types flow through the workflow and its transitions.

How it works

A workflow extends the abstract Workflow<Data, Output> class and implements getName, getDescription, and getTransitions. Each transition implements the ITransition<Data, Output> interface. Calling run(data, context?) does the following:
  1. Every transition’s isActive(data, context?) is evaluated to build the list of active transitions.
  2. Active transitions run in order. For each one: onStart fires, then handler(data, context?) produces an output, then onFinish fires with that output.
  3. The output of the last executed transition is returned.
  4. If a step throws, that transition’s onFail fires, the transitions executed so far are rolled back in reverse order, and a WorkflowException is thrown.
A transition implements these members:
MemberPurpose
getName()Unique, human-readable name (used in error messages).
getDescription()Short description of what the transition does.
isActive(data, context?)Whether this transition should run — return false to skip it.
handler(data, context?)Performs the work and returns the output.
rollback(data, context?)Undoes the work if a later transition fails.
onStart(data, context?)Fires before the handler runs.
onFinish(data, output, context?)Fires after the handler succeeds, with its output.
onFail(data, error, context?)Fires when the handler (or a hook) throws, with the error.
Every member may be synchronous or asynchronous. The optional context passed to run is forwarded to every member, so request-scoped values such as the current user or a request id reach each step. Rollback is precise: only transitions whose handler completed are rolled back, the transition that threw is not (its work never finished), and rollbacks are awaited before the WorkflowException is rethrown.
onStart → handler → onFinish        (success)
onStart → handler ✗ → onFail        (failure) → rollback (reverse) → WorkflowException

Decorator and usage

@decorator.transition()

Registers a transition class with the container. It accepts an optional scope (defaults to EContainerScope.Singleton).
import { decorator } from "@ooneex/workflow";
import type { ITransition } from "@ooneex/workflow";

interface OrderData extends Record<string, unknown> {
  orderId: string;
  amount: number;
}

@decorator.transition()
export class ChargePaymentTransition implements ITransition<OrderData, string> {
  public getName = () => "charge-payment";
  public getDescription = () => "Charges the customer for the order";
  public isActive = (data: OrderData) => data.amount > 0;

  public handler = async (data: OrderData) => {
    return paymentGateway.charge(data.orderId, data.amount);
  };

  public rollback = async (data: OrderData) => {
    await paymentGateway.refund(data.orderId);
  };

  public onStart = (data: OrderData) => logger.info(`Charging ${data.orderId}`);
  public onFinish = (data: OrderData, chargeId: string) => logger.info(`Charged ${chargeId}`);
  public onFail = (data: OrderData, error: unknown) => logger.error("Charge failed", error);
}

@decorator.workflow()

Registers a workflow class with the container. It also accepts an optional scope (defaults to EContainerScope.Singleton). List the transition classes in getTransitions() in the order they should run — return the class references only, never instances, since they are resolved from the container.
import { decorator, Workflow } from "@ooneex/workflow";
import type { WorkflowTransitionClassType } from "@ooneex/workflow";

@decorator.workflow()
export class CheckoutWorkflow extends Workflow<OrderData, string> {
  public getName = () => "checkout";
  public getDescription = () => "Processes an order from payment to receipt";

  public getTransitions = (): WorkflowTransitionClassType[] => [
    ChargePaymentTransition,
    SendReceiptTransition,
  ];
}
Resolve the workflow from the container and run it. The output is whatever the last executed transition returned:
import { container } from "@ooneex/container";
import { WorkflowException } from "@ooneex/workflow";

const workflow = container.get(CheckoutWorkflow);

try {
  const output = await workflow.run(
    { orderId: "ord_123", amount: 4200 },
    { requestId: "req_1", user: currentUser }, // optional context
  );
  console.log(output);
} catch (error) {
  if (error instanceof WorkflowException) {
    console.error(error.message); // Workflow "checkout" failed at transition "...".
    console.error(error.data); // { workflow, transition, error }
  }
}

Exceptions

A failed run throws a WorkflowException. It carries a machine-readable key, a human-readable message, an HTTP status, and a data object.
KeyWhen
WORKFLOW_RUN_FAILEDA transition’s handler or a lifecycle hook threw during run(). The executed transitions have already been rolled back in reverse order.
The message reads Workflow "<name>" failed at transition "<name>"., the status is 500 (Internal Server Error), and data is { workflow, transition, error }, where error is the original message (non-Error throwables are stringified).
import { WorkflowException } from "@ooneex/workflow";

try {
  await workflow.run(data);
} catch (error) {
  if (error instanceof WorkflowException) {
    logger.error(`Workflow error [${error.key}]: ${error.message}`, error.data);
  } else {
    throw error;
  }
}

Best practices

  • One concern per transition. Keep each transition focused on a single, nameable action so it stays easy to test and roll back.
  • Make rollback the true inverse of handler. Whatever side effect a handler creates — a charge, a reservation, a record — its rollback should undo it.
  • Guard with isActive. Skip steps that don’t apply to the current data instead of branching inside the handler.
  • Order transitions deliberately. They run in the order returned by getTransitions(), and roll back in reverse — put reversible, cheap steps first where you can.
  • Return class references, never instances. Transitions are resolved from the container; list the classes in getTransitions().
  • Use context for request-scoped values. Pass the current user or request id as the second run argument rather than threading them through Data.
  • Catch WorkflowException at the boundary. Inspect its stable key and data to report the failing workflow and transition.

CLI command

Scaffold a workflow with workflow:create and each of its steps with workflow:transition:create. The generators write the class and a matching test under the target module and install @ooneex/workflow if it is missing.
# Interactive: prompts for the name
ooneex workflow:create
ooneex workflow:transition:create

# Provide the name
ooneex workflow:create --name=Checkout
ooneex workflow:transition:create --name=ChargePayment

# Target a module and overwrite
ooneex workflow:create --name=Checkout --module=orders --override
OptionDescriptionDefault
--nameClass name. workflow:create appends the Workflow suffix; workflow:transition:create appends Transition.Prompted if omitted
--moduleTarget module the class is generated into.shared
--overrideOverwrite an existing class without prompting.false
workflow:create writes the class under modules/<module>/src/workflows/<Name>Workflow.ts as a Workflow subclass with an empty transition list, ready for you to fill in:
import type { WorkflowTransitionClassType } from "@ooneex/workflow";
import { decorator, Workflow } from "@ooneex/workflow";

/** Data threaded through CheckoutWorkflow and each of its transitions. */
export type CheckoutWorkflowDataType = Record<string, unknown>;

@decorator.workflow()
export class CheckoutWorkflow extends Workflow<CheckoutWorkflowDataType> {
  public getName = (): string => "checkout";

  public getDescription = (): string => "";

  /** Transitions run in order; each one's `isActive` decides if it executes. */
  public getTransitions = (): WorkflowTransitionClassType[] => [];
}
workflow:transition:create writes the class under modules/<module>/src/workflows/transitions/<Name>Transition.ts as an ITransition stub with every member ready to implement:
import type { ITransition } from "@ooneex/workflow";
import { decorator } from "@ooneex/workflow";

/** Data threaded through the workflow run and read by ChargePaymentTransition. */
export type ChargePaymentTransitionDataType = Record<string, unknown>;

@decorator.transition()
export class ChargePaymentTransition
  implements ITransition<ChargePaymentTransitionDataType, unknown>
{
  public getName = (): string => "charge-payment";

  public getDescription = (): string => "";

  /** Whether this transition should run for the given data. */
  public isActive = (_data: ChargePaymentTransitionDataType): boolean => true;

  /** Performs the transition's work and returns its output. */
  public handler = async (data: ChargePaymentTransitionDataType): Promise<unknown> => {
    return data;
  };

  /** Undoes handler() when a later transition in the workflow fails. */
  public rollback = async (_data: ChargePaymentTransitionDataType): Promise<void> => {};

  /** Runs before handler(). */
  public onStart = async (_data: ChargePaymentTransitionDataType): Promise<void> => {};

  /** Runs after handler() resolves successfully. */
  public onFinish = async (
    _data: ChargePaymentTransitionDataType,
    _output: unknown,
  ): Promise<void> => {};

  /** Runs when handler() throws. */
  public onFail = async (
    _data: ChargePaymentTransitionDataType,
    _error: unknown,
  ): Promise<void> => {};
}
See workflow:create and workflow:transition:create for the full command references.

Use with Claude and Codex

The generators ship matching workflow:create and workflow:transition:create skills. They run the scaffold and then guide your AI agent through completing the workflow — defining the Data type, listing the transitions in order, and implementing each transition’s isActive, handler, and rollback. Initialize the skills once for your agent:
ooneex claude:init
Then ask Claude in natural language — it maps the request to the generators, runs them, and fills in the implementation:
Prompt
Create an order workflow with pending, paid, and shipped states.
For example, the prompt above maps to workflow:create --name=Order, then a workflow:transition:create for each step, wiring the transitions into getTransitions() in order.