Forks in Depth
Forks are how a pipeline decides where to go next. They live on a node’s forks array and are evaluated in order after the node’s action runs. The first fork whose condition returns true wins; the rest are skipped.
First-match-wins ordering
Section titled “First-match-wins ordering”import { definePipeline, node, action, fork, target, terminal } from '@tde.io/plexis';
type Ctx = { score: number };
const classify = definePipeline<Ctx>('classify', () => { node('evaluate', () => { action(async (ctx) => ctx); fork('grade-a', target('grade-a'), (ctx) => ctx.score >= 90); fork('grade-b', target('grade-b'), (ctx) => ctx.score >= 80); fork('grade-c', target('grade-c'), (ctx) => ctx.score >= 70); fork('grade-d', target('grade-d'), (ctx) => ctx.score >= 60); fork('grade-f', target('grade-f')); }); node('grade-a', terminal()); node('grade-b', terminal()); node('grade-c', terminal()); node('grade-d', terminal()); node('grade-f', terminal()); return { initial: 'evaluate' };});
const result = await classify.run({ score: 85 });// score >= 90 → false (skip)// score >= 80 → true (wins)// result.finalNode === 'grade-b'A score of 85 satisfies score >= 80 and score >= 70, but only the first match runs — grade-b is selected and grade-c is never evaluated.
The catch-all fork
Section titled “The catch-all fork”A fork with no condition always matches. Place it last to handle any input not caught by earlier conditions.
import { definePipeline, node, action, fork, target, terminal } from '@tde.io/plexis';
type Ctx = { status: string };
const route = definePipeline<Ctx>('route', () => { node('dispatch', () => { fork('approved', target('process'), (ctx) => ctx.status === 'approved'); fork('review', target('hold'), (ctx) => ctx.status === 'review'); fork('default', target('reject')); }); node('process', terminal()); node('hold', terminal()); node('reject', terminal()); return { initial: 'dispatch' };});No matching fork stops the run
Section titled “No matching fork stops the run”If the forks array is empty, or every condition returns false, the pipeline stops at the current node with status: 'stopped' in the result. This is not an error — it is the intended behavior for optional continuations.
import { definePipeline, node, action, fork, target, terminal } from '@tde.io/plexis';
type Ctx = { sendNotification: boolean };
const maybe = definePipeline<Ctx>('maybe', () => { node('check', () => { fork('notify', target('notify'), (ctx) => ctx.sendNotification); }); node('notify', terminal()); return { initial: 'check' };});
const result = await maybe.run({ sendNotification: false });// result.status === 'stopped'// result.finalNode === 'check'Distinguish this from a terminal node: status: 'completed' means a terminal node (registered with terminal()) was reached; status: 'stopped' means execution ended because no fork matched.
Fork conditions run after the action
Section titled “Fork conditions run after the action”Conditions receive the context after the node’s action has run and its patch has been merged. Write conditions against the updated context, not the input context.
import { definePipeline, node, action, fork, target, terminal } from '@tde.io/plexis';
type Ctx = { raw: number; score: number };
const score = definePipeline<Ctx>('score', () => { node('compute', () => { action(async (ctx) => ({ score: ctx.raw * 10 })); fork('pass', target('pass'), (ctx) => ctx.score >= 100); fork('fail', target('fail')); }); node('pass', terminal()); node('fail', terminal()); return { initial: 'compute' };});
const result = await score.run({ raw: 10, score: 0 });// action sets score: 100 → condition ctx.score >= 100 is true// result.finalNode === 'pass'For the full fork() signature, see the Registration Helpers reference.