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
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 anemit action and subscribe to it by adding an Event Listener whose topic field references it.
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 (SCREAMING_SNAKE_CASE is enforced —CURRENT_SHIFT, not currentShift), a type (one of number, text, boolean), an optional description, 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.
In results data, each state variable’s initial value lands as a
t=0 row in the state_variable_activity table — except variables aliased into a sub-model via state variable mappings, which delegate to the parent and emit no row of their own.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.
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 theon_ prefix and use spaces: entity created in the UI corresponds to on_entity_created in the schema.
A few things to notice:
entity entered/entity exitedonly fire on Buffer and Station. Processes don’t emit them: useprocess started/process completed. Routers don’t emit them: useentity routed.- The multi-entity events are where aggregations live.
contents changedon a Buffer or Station (on_contents_changedin the schema — the same event name on both),batch assembledon a Combiner, andbatch createdon a Separator all run in multi-entity context — conditions and assignments there use aggregation functions (SUM,MEAN,COUNT,MAX,MIN,ANY,ALL,MODE,N_UNIQUE) over the entities in scope — for exampleMEAN(weight)for the average across the batch. A buffer threshold check isCOUNT() > 10oncontents changed, not a per-entity condition. slot freed/slot workingare on Resources, not Stations, and run in no-entity context. The naming is unusual:slot workingfires when a slot transitions from FREE to allocated.- Per-entity values in multi-entity context (
ENTITY_TYPE,ENTITY_AGE) are only reachable inside an aggregation body —MAX(ENTITY_AGE)works, bareENTITY_AGEdoesn’t. In single-entity context both are available directly (ENTITY_AGEisSIM_TIMEminus the entity’s creation time). entity_typefilter is only valid on Combineron_entity_consumedand Separatoron_entity_created. Other event hooks fire for every entity regardless of type.
Within one hook, actions run sequentially — across hooks, order is not specified. The
actions[] list of a single hook or listener executes in order, so an assign followed by an emit in the same rule is safe. But if two separate hooks 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.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")orassign(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 entities on demand. Valid on Sources (releases new entities) and Buffers (releases queued entities).quantityaccepts an integer or a DSL expression — evaluated in no-entity context on a Source, and in multi-entity context over the current contents on a Buffer (aggregations apply). The result is truncated to a non-negative integer; zero or less releases nothing. An optionalfilter(per-entity boolean DSL) selects which queued entities qualify — it’s Buffer-only and rejected by the validator on a Source.set capacity(set_capacity): change the capacity of a Resource at runtime.
Per-Component Action Restrictions
Not every action is valid on every component:
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 optionalcondition 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 and contents-changed hooks).
Conditions live at the hook level, not the action level: a hook’s actions either all run or all skip.
SELF
Inside any hook or listener,SELF resolves to the hosting component’s id at runtime. That makes rules copy-pasteable across components: BUFFER_LEVEL(SELF), STATION_WIP(SELF), RESOURCE_AVAILABLE(SELF) all follow the rule wherever it’s attached. SELF is not available in scheduled actions (no component context there) or in top-level component fields.
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 topiccondition(optional): boolean DSL expression (the code builder calls this parameterwhen; the stored schema field iscondition)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:Subscriptions Are Static
Subscriptions are declared at model build time and never change at runtime. There’s nosubscribe 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 anevent_hooks[] array on a ModelNode, but no canonical lifecycle events are documented for ModelNodes. Listeners (topic-based) work fully and are the right tool for ModelNode-level coordination. Topics and state variables bridge across the ModelNode boundary via mappings — a bridged emission shows up as two rows in the emission dataset (origin + delivery).
Avoiding Event Loops
Event-driven rules can feed themselves. Keep three guidelines in mind:- Don’t let a component’s own lifecycle trigger its own
release. A Source whoseentity createdhook releases another entity, or a Buffer whoseentity exitedhook releases the next one unconditionally, is a self-amplifying loop — drive releases from downstream signals (a kanban topic) instead. - Keep the topic graph a DAG in your head. If a listener on topic A emits topic B, and a listener on B emits A, every emission cascades forever. Chain topics in one direction and gate re-entrant paths with a
conditionon a state variable. - Flow-graph cycles are fine. Routing entities back through an upstream Process (rework loops) is a perfectly valid model — the hazard is event loops, not connection loops.
Scheduled Actions
A Schedule can also fire actions at specific simulation times. Schedules are a separate mechanism from Event Hooks: a schedule fires its own actions at declared times and does not fire any component lifecycle hook — the topic is the bridge between the two. Scheduled actions are more restricted than Event Hook actions:- Only
assignandemitare valid as scheduled actions. - Component-bound actions (
pause,resume,set capacity,release) are not available in schedules. Use a scheduledemitto a topic, then attach an Event Listener that listens for that topic and performs the component-bound action.
Authoring Events with Dexter
When Dexter wires up event logic, it uses the simulation builders — the same vocabulary this page describes:sim.assign, sim.emit, sim.pause, sim.resume, sim.release_entity, sim.set_capacity); sim.topic(name) declares topics and sim.state(name, type=..., initial=...) declares state variables. Component builders also expose the common events directly as on_* keyword arguments (on_entered, on_created, on_contents_changed, …) — the explicit sim.hook(...) form is for when you also need an entity_type filter or a condition.
The Event System in Results Data
Three results tables capture what the event system actually did during a run:event_lookup— a dimension table with one row per action of every hook and listener, written at setup (nothing appends during the run). Columns includecomponent_id,config_type("hook"/"listener"),event_or_topic,entity_type_id,condition,action_index,action_type, and the typed columnsvariable_name/value_expr/topic/capacity_expr(null forpause/resume/release_entityactions).topic_activity— one row per executedemit.trigger_sourcedistinguishesschedule/hook/listener/bridge;trigger_component_idis null on schedule and bridge rows;bridged_fromcarries the source-side topic (with its hierarchical prefix) on bridge rows. Schedule-fired emits carry whole-second timestamps; hook and listener emits keep sub-second precision.state_variable_activity— one row perassign, withvariable_name,value_type, and typedvalue_number/value_text/value_booleancolumns.trigger_component_idis null on scheduled assigns and on thet=0initial-value rows.
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 arelease 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)

