Skip to content
@tde.io/plexis  ·  npm install @tde.io/plexis  ·  npm  ·  GitHub

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.

Every call to domain.follow(event, payload) runs these steps in sequence:

  1. Guard check — if the event has a guard function, it is evaluated first. If it returns false, the follow is blocked immediately; no context changes, no side effects.
  2. exit — the current phase’s exit hook runs. The patch it returns is merged into context.
  3. on action — the event’s action (registered via the ambient action(fn) helper inside the on setup function) runs. Its patch is merged into context.
  4. on pipeline — if the event has a pipeline, it runs against the updated context. Its final context becomes the new context.
  5. Phase change — the domain moves to the target phase.
  6. enter — the new phase’s enter hook runs. Its patch is merged into context.
  7. Phase entry pipeline — if the new phase has a pipeline, it runs against the updated context.
  8. History and subscribers — the event is recorded in history, and all subscribe listeners 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.

Every call to pipeline.run(context, input?) follows this loop:

  1. Start at initial — execution begins at the node named in the setup return value.
  2. Run node action — the node’s action runs. Its patch is merged into context.
  3. Evaluate forks — forks are checked in declaration order. The first fork whose condition returns true wins.
  4. Follow fork target — execution moves to the fork target (a node ID string or a sub-Pipeline).
  5. Repeat — steps 2–4 repeat until a terminal node or no matching fork.
  6. 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.