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

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, synchronously
const 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 constructed
console.log(order.phase); // 'pending'

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 throws
when('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 builder
node('validate', terminal());
// PlexisError: code 'BUILDER_CLOSED'
// Called in defineDomain body but outside a when() setup
import { defineDomain, on } from '@tde.io/plexis';
defineDomain('d', () => {
on('submit', target('x')); // throws — must be inside when()
return { context: {}, initial: 'x' };
});

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 immediately
import { 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' };
});

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() or node() in a timer callback or promise inside the setup function.
  • Calling on()/enter()/exit() outside a when() setup, or action()/fork() outside a node() 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.