Execution and Lifecycle
When code calls domain.follow() or pipeline.run(), the runtime follows a deterministic, ordered sequence of steps. Knowing this order tells you exactly when your handlers fire, in what context, and where to place side effects that depend on prior steps having completed.
Domain: the follow() order
Section titled “Domain: the follow() order”Every call to domain.follow(event, payload) runs these steps in sequence:
- Guard check — if the event has a
guardfunction, it is evaluated first. If it returnsfalse, the follow is blocked immediately; no context changes, no side effects. exit— the current phase’sexithook runs. The patch it returns is merged into context.onaction — the event’saction(registered via the ambientaction(fn)helper inside theonsetup function) runs. Its patch is merged into context.onpipeline — if the event has apipeline, it runs against the updated context. Its final context becomes the new context.- Phase change — the domain moves to the target phase.
enter— the new phase’senterhook runs. Its patch is merged into context.- Phase entry pipeline — if the new phase has a
pipeline, it runs against the updated context. - History and subscribers — the event is recorded in history, and all
subscribelisteners are called with the new snapshot.
import { defineDomain, when, enter, exit, on, action, target } from '@tde.io/plexis';
const workflow = defineDomain('workflow', () => { when('draft', () => { exit(async (ctx) => { // Step 2 — runs when leaving 'draft' return { leftAt: 'draft' }; }); on('submit', () => { action(async (ctx) => { // Step 3 — ctx now has leftAt from exit return { submittedBy: 'user_1' }; }); return target('review'); }); });
when('review', () => { enter(async (ctx) => { // Step 6 — ctx now has leftAt and submittedBy return { enteredReviewAt: Date.now() }; }); });
return { context: {}, initial: 'draft' };});Where side effects belong: use enter/exit for phase-scoped side effects (logging, metrics) and event actions for event-scoped ones (validation, API calls triggered by a specific event). Subscribers (step 8) are ideal for reacting to the completed event from outside the domain.
Pipeline: the run loop
Section titled “Pipeline: the run loop”Every call to pipeline.run(context, input?) follows this loop:
- Start at
initial— execution begins at the node named in the setup return value. - Run node action — the node’s
actionruns. Its patch is merged into context. - Evaluate forks — forks are checked in declaration order. The first fork whose
conditionreturnstruewins. - Follow fork target — execution moves to the fork target (a node ID string or a sub-Pipeline).
- Repeat — steps 2–4 repeat until a terminal node or no matching fork.
- Return
PipelineRunResult— status is'completed'if a terminal was reached,'stopped'if no fork matched.
import { definePipeline, node, action, fork, target, terminal } from '@tde.io/plexis';
const process = definePipeline('process', () => { node('validate', () => { action(async (ctx) => ({ validated: true })); fork('valid', target('charge'), (ctx) => ctx.validated && ctx.amount > 0); fork('invalid', target('reject'), (ctx) => !ctx.validated || ctx.amount <= 0); }); node('charge', terminal()); node('reject', terminal());
return { initial: 'validate' };});
const result = await process.run({ validated: false, amount: 50 });// Forks are evaluated after the action merges { validated: true }// ctx.validated is now true, ctx.amount > 0, so 'charge' wins// result.finalNode === 'charge'Forks evaluate after the action — the action runs first and its patch is merged before any fork condition is checked. This means fork conditions always see the result of the node’s action.
No matching fork stops the run — if all fork conditions return false, the pipeline stops at the current node with status: 'stopped'. This is not an error; it is a valid terminal-like outcome.
For full method signatures, see the Domain reference and Pipeline reference.