Context and Patches
Plexis never mutates context. Every handler that can modify state does so by returning a patch — a partial object that the runtime merges over the previous context. Understanding this model makes every action, enter, exit, and node action predictable.
What context is
Section titled “What context is”Context is a plain object you provide when defining a domain or calling pipeline.run(). The runtime holds one canonical copy at any moment. Between transitions, that copy is stable and safe to read.
import { defineDomain, when, on, terminal } from '@tde.io/plexis';
type OrderCtx = { orderId: string; total: number; paid: boolean };
const order = defineDomain<OrderCtx>('order', () => { when('pending', () => { on('pay', () => { action(async (ctx) => ({ paid: true })); return target('paid'); }); }); when('paid', terminal());
return { context: { orderId: 'ord_1', total: 99, paid: false }, initial: 'pending', };});
console.log(order.context.paid); // false
await order.follow('pay');
console.log(order.context.paid); // trueconsole.log(order.context.total); // 99 — unchanged keys are preservedPatches are shallow-merged
Section titled “Patches are shallow-merged”The runtime applies the patch with { ...previous, ...patch }. This means:
- Keys in the patch overwrite the same keys in previous context.
- Keys not present in the patch survive unchanged.
- Returning
undefined,null, or nothing is equivalent to returning{}— no change.
// Only the paid field changes; orderId and total surviveaction: async (ctx) => ({ paid: true })
// Returning nothing — context unchangedenter: async (ctx) => {}
// Replacing a key with undefined removes it from TypeScript's view// but that is unusual; prefer explicit false/null for optional flagsaction: async (ctx) => ({ discount: undefined })Context is not mutated in place
Section titled “Context is not mutated in place”Handlers receive context as a read-only snapshot. Mutating ctx directly has no effect — the runtime does not watch the object; it reads the return value.
// This does nothing — the mutation is invisible to the runtimeaction: async (ctx) => { (ctx as any).paid = true; // has no effect // return value is undefined → no change}
// This is correctaction: async (ctx) => ({ paid: true })Async handlers
Section titled “Async handlers”Handlers can be async. The runtime uses await throughout, so returning a Promise<Partial<TContext>> works exactly like returning the value directly.
action: async (ctx, input) => { const result = await externalService.verify(ctx.orderId); return { verified: result.ok };}Overriding the default merge
Section titled “Overriding the default merge”The default shallow merge is right for most use cases. When you need nested merging, array appends, or key deletion, supply a custom merge in the definition options. See Custom Merge in Patterns.