@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, andonFail. - Container-managed. Register workflows and transitions with a decorator and resolve them from
@ooneex/container. - Type-safe. Generic
DataandOutputtypes flow through the workflow and its transitions.
How it works
A workflow extends the abstractWorkflow<Data, Output> class and implements getName, getDescription, and getTransitions. Each transition implements the ITransition<Data, Output> interface. Calling run(data, context?) does the following:
- Every transition’s
isActive(data, context?)is evaluated to build the list of active transitions. - Active transitions run in order. For each one:
onStartfires, thenhandler(data, context?)produces an output, thenonFinishfires with that output. - The output of the last executed transition is returned.
- If a step throws, that transition’s
onFailfires, the transitions executed so far are rolled back in reverse order, and aWorkflowExceptionis thrown.
| Member | Purpose |
|---|---|
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. |
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.
Decorator and usage
@decorator.transition()
Registers a transition class with the container. It accepts an optional scope (defaults to EContainerScope.Singleton).
@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.
run it. The output is whatever the last executed transition returned:
Exceptions
A failed run throws aWorkflowException. It carries a machine-readable key, a human-readable message, an HTTP status, and a data object.
| Key | When |
|---|---|
WORKFLOW_RUN_FAILED | A transition’s handler or a lifecycle hook threw during run(). The executed transitions have already been rolled back in reverse order. |
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).
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
rollbackthe true inverse ofhandler. 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
contextfor request-scoped values. Pass the current user or request id as the secondrunargument rather than threading them throughData. - Catch
WorkflowExceptionat the boundary. Inspect its stablekeyanddatato report the failing workflow and transition.
CLI command
Scaffold a workflow withworkflow: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.
| Option | Description | Default |
|---|---|---|
--name | Class name. workflow:create appends the Workflow suffix; workflow:transition:create appends Transition. | Prompted if omitted |
--module | Target module the class is generated into. | shared |
--override | Overwrite 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:
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:
Use with Claude and Codex
The generators ship matchingworkflow: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:
- Claude
- Codex
Prompt
workflow:create --name=Order, then a workflow:transition:create for each step, wiring the transitions into getTransitions() in order.