Definition Lifecycle
Defining a domain or pipeline in Plexis has one rule: the setup function runs synchronously, right now, when you call defineDomain or definePipeline. Registration helpers — when, on, enter, exit, node, action, fork, terminal — only work inside that synchronous call.
The setup function runs at definition time
Section titled “The setup function runs at definition time”When you call defineDomain(id, setup), the runtime immediately calls setup(). Whatever when() and on() registrations happen inside that call are captured. When setup() returns, the builder closes. The domain or pipeline object is fully constructed before execution continues.
import { defineDomain, when, on, terminal } from '@tde.io/plexis';
// setup() runs here, synchronouslyconst order = defineDomain('order', () => { when('pending', () => { on('submit', target('processing')); }); when('processing', terminal());
// Return the initial config — must be sync return { context: { orderId: '' }, initial: 'pending' };});
// By this line, order is fully constructedconsole.log(order.phase); // 'pending'Builder scope
Section titled “Builder scope”defineDomain and definePipeline each open a builder scope before calling setup() and close it after. The registration helpers — when, node, fork — write into the active scope. Each when(id, fn) call opens a nested scope that on, enter, and exit write into.
This is why helpers called outside a setup function throw:
import { when } from '@tde.io/plexis';
// No active builder scope — this throwswhen('pending', () => {});// PlexisError: code 'BUILDER_CLOSED'// "when() called outside an active defineDomain scope"The same applies to node() called outside definePipeline, and on()/enter()/exit() called outside a when() setup:
import { node, terminal, on } from '@tde.io/plexis';
// Called at module scope — no active buildernode('validate', terminal());// PlexisError: code 'BUILDER_CLOSED'
// Called in defineDomain body but outside a when() setupimport { defineDomain, on } from '@tde.io/plexis';defineDomain('d', () => { on('submit', target('x')); // throws — must be inside when() return { context: {}, initial: 'x' };});Why synchronous setup
Section titled “Why synchronous setup”Async setup would mean the domain could be used before it is ready. Synchronous setup makes the definition instant: one call, fully configured, no await needed. This keeps definitions safe to write at module scope or in factory functions without coordinating initialization order.
// Safe at module scope — fully ready immediatelyimport { defineDomain, definePipeline, when, on, node, fork, terminal } from '@tde.io/plexis';
const notifyUser = definePipeline('notify-user', () => { node('send', terminal()); return { initial: 'send' };});
const signup = defineDomain('signup', () => { when('new', () => { on('activate', () => { pipeline(notifyUser); return target('active'); }); }); when('active', terminal()); return { context: {}, initial: 'new' };});The BUILDER_CLOSED error
Section titled “The BUILDER_CLOSED error”If a registration helper is called after its enclosing defineDomain/definePipeline has returned, the runtime throws PlexisError with code BUILDER_CLOSED. Common causes:
- Calling
when()ornode()in a timer callback or promise inside the setup function. - Calling
on()/enter()/exit()outside awhen()setup, oraction()/fork()outside anode()setup. - Nested async inside setup (setup must be synchronous; async helpers are not supported at registration time).
The error is intentional: it prevents silent misconfiguration where a registration looks like it succeeded but was ignored because the scope was gone.
For the full list of error codes, see the Errors reference.