Overview
The Event System is what elevates ProDex from a process-flow simulator to a system that can model sophisticated operational logic. It’s built on four primitives:
- Topics — pub/sub messaging between components
- State Variables — mutable runtime state that lives on the model
- Event Hooks — actions that fire on a component’s own lifecycle events
- Event Listeners — actions that fire when a subscribed topic is emitted
Together they enable patterns that aren’t expressible with connection-based flow alone: pull-based production (kanban), backpressure, shift-driven capacity changes, cross-component coordination. A user who only knows about sources, processes, and sinks is using a fraction of what the platform can model.
Every flow component in the Modeler exposes both an Event Hooks and an Event Listeners section in its configuration panel — they’re distinct first-class objects, not variants of the same thing. Topics and State Variables are managed in the Lookups modal, opened from the Lookups button at the bottom of the Modeler’s Library panel. The modal has four tabs: Constants, Lookup Tables, State Variables, and Topics.
Topics
A Topic is a named channel components can publish to and subscribe to. When a component publishes on a topic, every component that has an Event Listener subscribed to that topic reacts.
Topics enable coordination that isn’t expressible through flow connections alone — one component can trigger behavior in another without being directly wired to it.
Create and rename Topics in the Topics tab of the Lookups modal. Once a Topic exists, components publish to it via an emit action and subscribe to it by adding an Event Listener whose topic field references it.
Topics carry no payload. An emit action takes only a topic name; an Event Listener fires on the topic name alone with no message data. If a downstream listener needs information about why the topic was emitted, the canonical pattern is to set a state variable just before the emit and have the listener read that state variable.
Example — kanban signal: when a downstream buffer drops below a threshold, an event hook emits the topic replenish. An upstream Source has an Event Listener subscribed to replenish that runs a release action — generating more material.
State Variables
A State Variable is a named, mutable value that lives on the model (not on any individual entity). State variables persist across entities — they retain their value as entities flow through the model, and they can be read from any expression.
Use state variables for anything that represents the state of the system rather than the state of a single entity: current shift, machine uptime counters, inventory levels, demand signals, rolling averages.
Define State Variables in the State Variables tab of the Lookups modal. Each has a name, a type (one of number, text, boolean), and a required initial_value matching the declared type. Once defined, read them by name from any expression and write to them with an assign action in any Event Hook, Event Listener, or scheduled action.
State Variables vs. Entity Attributes
- Entity attribute — belongs to a specific entity and moves with it through the flow. Defined on the entity type.
- State Variable — belongs to the model and persists independently of entities. Defined at the model level via the Lookups modal.
If you want the same value visible to every component at every moment, use a state variable. If you want the value to track a specific entity as it moves through the flow, use an attribute.
Event Hooks
An Event Hook is a rule on a component that fires actions when one of that component’s own lifecycle events occurs. Hooks live in the Event Hooks section of every flow component’s config panel — pick the event from a dropdown, optionally add a condition, and configure one or more actions.
Canonical Events per Component
Each event is only valid on specific component types and runs its expressions in a specific context. UI labels strip the on_ prefix and use spaces — entity created in the UI corresponds to on_entity_created in the schema.
| Component | Events | Context |
|---|
| Source | entity created | single-entity (the newly created entity) |
| Combiner | entity consumed, batch assembled, entity created | single-entity / multi-entity (for batch assembled) / single-entity (output) |
| Separator | entity consumed, entity created, batch created | single-entity / single-entity (each output) / multi-entity (all outputs) |
| Transformer | entity consumed, entity created | single-entity |
| Buffer | entity entered, entity exited | single-entity |
| Station | entity entered, entity exited | single-entity |
| Process | process started, process completed | single-entity |
| Router | entity routed | single-entity |
| Resource | slot freed, slot working | no-entity |
| Sink | entity terminated | single-entity |
A few things to notice:
entity entered / entity exited only fire on Buffer and Station. Processes don’t emit them — use process started / process completed. Routers don’t emit them — use entity routed.
slot freed / slot working are on Resources, not Stations. The naming is unusual — slot working fires when a slot transitions from FREE to allocated.
batch created is a Separator event in multi-entity context over the outputs. Combiner’s parallel event is batch assembled, also multi-entity but over the inputs.
entity_type filter is only valid on Combiner on_entity_consumed and Separator on_entity_created. Other event hooks fire for every entity regardless of type.
Hook execution order is undefined when multiple hooks fire on the same event. If two hooks on the same component subscribe to the same event, the order they run in isn’t specified — don’t rely on side effects from one becoming visible to the other inside a single event.
Action Types
When a hook fires, it executes one or more actions. The Action dropdown offers (UI labels in code, schema names in parens):
assign — write to a state variable. Example: assign(CURRENT_SHIFT, "night") or assign(WIP_COUNT, WIP_COUNT + 1).
emit — publish a message to a topic. Example: emit("replenish").
pause — pause a component (stops accepting and processing).
resume — resume a paused component.
release (release_entity) — release an entity on demand. Valid on Sources (releases a new entity) and Buffers (releases the next queued entity from a paused or hold-mode buffer).
set capacity (set_capacity) — change the capacity of a Resource at runtime.
Per-Component Action Restrictions
Not every action is valid on every component:
| Component | assign | emit | release | pause | resume | set capacity |
|---|
| Source | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Buffer | ✓ | ✓ | ✓ | ✓ | ✓ | |
| Resource | ✓ | ✓ | | | | ✓ |
| Station, Process, Router, Sink, Combiner, Separator, Transformer | ✓ | ✓ | | | | |
In short: set capacity is valid only on Resource. release / pause / resume are valid only on Source and Buffer. assign and emit are valid everywhere.
Conditions
Every event hook has an optional condition field — a boolean DSL expression evaluated each time the event fires. If false, the action list is skipped. The condition runs in the same context as the actions (single-entity for entity-bound hooks, no-entity for resource slot hooks, multi-entity for batch hooks).
Conditions live at the hook level, not the action level — a hook’s actions either all run or all skip.
Event Listeners
An Event Listener is a rule on a component that fires actions when a subscribed topic is emitted, decoupled from any lifecycle event. Listeners live in the Event Listeners section of every flow component’s config panel — a sibling to Event Hooks, with its own Add Listener button.
Each listener has:
topic (required) — the name of a declared topic
condition (optional) — boolean DSL expression
actions[] — same six action types as Event Hooks, with the same per-component restrictions
Event Listeners always run in no-entity context, regardless of host component. A listener on a Source can’t reference entity attributes, even though a Source’s on_entity_created hook can. Listeners are reactions to model-level signals, not to specific entities — there’s no entity in scope unless an action explicitly pulls one in (e.g., release).
Hooks vs. Listeners
The two reaction primitives look similar but behave differently:
| Event Hook | Event Listener |
|---|
| Triggered by | Component’s own lifecycle event | Topic emission |
| Context | Varies by event (no-entity / single / multi) | Always no-entity |
| Where it lives | Component’s event_hooks[] array | Component’s event_listeners[] array |
| Subscription | Implicit (component fires its own lifecycle) | Explicit (topic field references a declared topic) |
| When to reach for it | Reacting to this component’s behavior | Reacting to model-level coordination signals |
Subscriptions Are Static
Subscriptions are declared at model build time and never change at runtime. There’s no subscribe or unsubscribe action. If you need different listeners active under different conditions, gate them with the listener’s condition field.
ModelNode and the Event System
Model Nodes can host Event Listeners but not lifecycle Event Hooks — the schema accepts an event_hooks[] array on a ModelNode, but no canonical lifecycle events are defined for ModelNodes, so any authored hook would fail validation. Listeners (topic-based) work fully and are the right tool for ModelNode-level coordination.
Loop-Back Validation
ProDex prevents three specific runaway patterns:
- Direct self-loops — a Source’s
on_entity_created hook cannot run a release action; a Buffer’s on_entity_exited hook cannot run a release action. These two specific combinations are forbidden by the validator.
- Topic graph acyclicity — the directed graph of topic emissions (where an edge from topic A to topic B exists when a listener on A emits B) must be acyclic. Cycles are caught at validation, before the simulator runs.
- Flow-graph cycles are explicitly allowed. Routing entities back through an upstream Process (rework loops) is a valid model — the validator only flags event-driven loops, not connection-graph cycles.
Scheduled Actions
A Schedule can also fire actions at specific simulation times, independent of any component lifecycle event. Scheduled actions are more restricted than Event Hook actions:
- Only
assign and emit are valid as scheduled actions.
- Component-bound actions (
pause, resume, set capacity, release) are not available in schedules. Use a scheduled emit to a topic, then attach an Event Listener that listens for that topic and performs the component-bound action.
This restriction keeps schedules declarative — a schedule declares “at time T, the world is in state X,” and state changes propagate through the event system.
Common Patterns
Kanban / Pull-Based Production. Downstream components signal upstream sources when they need more material. Combine a state variable (current WIP) with an Event Hook that emits to a topic when WIP drops below a threshold, plus an Event Listener on the Source that listens for the topic and runs a release action.
Backpressure. When a downstream buffer is full, pause or slow upstream arrivals. An Event Hook watches buffer level changes and updates a state variable that the Source’s arrival-rate expression reads.
Shift-Driven Capacity Changes. Resources change capacity by time of day. A scheduled emit on a shift_start_night topic fires at the shift boundary; an Event Listener on the Resource listens for that topic and runs set capacity with the night value.
Cross-Component Coordination. Two distant components that aren’t connected by flow can still coordinate through a shared topic. One publishes, the other reacts via an Event Listener.
When to Reach For the Event System
Most simple models don’t need events — a Source, a few Processes, a Sink, and you’re running. Reach for events when:
- You need behavior that spans multiple components without direct flow
- You need the model to react to operational rules (shift schedules, priority changes, demand surges)
- You’re modeling lean or pull systems where downstream pulls from upstream
- You need to track and react to cross-cutting metrics (rolling WIP, utilization)
If you find yourself duplicating logic in many expressions to coordinate behavior, that’s a signal to introduce a State Variable or a Topic.