Skip to main content

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. If you’re still getting oriented on components and flow, read the Simulation overview first — this page covers the coordination layer on top. 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 (SCREAMING_SNAKE_CASE is enforcedCURRENT_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.
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. 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.
  • The multi-entity events are where aggregations live. contents changed on a Buffer or Station (on_contents_changed in the schema — the same event name on both), batch assembled on a Combiner, and batch created on 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 example MEAN(weight) for the average across the batch. A buffer threshold check is COUNT() > 10 on contents changed, not a per-entity condition.
  • slot freed / slot working are on Resources, not Stations, and run in no-entity context. The naming is unusual: slot working fires 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, bare ENTITY_AGE doesn’t. In single-entity context both are available directly (ENTITY_AGE is SIM_TIME minus the entity’s creation time).
  • 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.
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") 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 entities on demand. Valid on Sources (releases new entities) and Buffers (releases queued entities). quantity accepts 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 optional filter (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.
Capacity decreases are non-preemptive. set_capacity(0) on a Resource does not interrupt a job that’s already running — the in-flight unit finishes first, then the slot goes away. When you model breakdowns or downtime windows this way, expect the last job to spill past the boundary.

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 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 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 topic
  • condition (optional): boolean DSL expression (the code builder calls this parameter when; the stored schema field is condition)
  • 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 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 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:
  1. Don’t let a component’s own lifecycle trigger its own release. A Source whose entity created hook releases another entity, or a Buffer whose entity exited hook releases the next one unconditionally, is a self-amplifying loop — drive releases from downstream signals (a kanban topic) instead.
  2. 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 condition on a state variable.
  3. 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 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.

Authoring Events with Dexter

When Dexter wires up event logic, it uses the simulation builders — the same vocabulary this page describes:
Action builders mirror the six action types (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 include component_id, config_type ("hook" / "listener"), event_or_topic, entity_type_id, condition, action_index, action_type, and the typed columns variable_name / value_expr / topic / capacity_expr (null for pause/resume/release_entity actions).
  • topic_activity — one row per executed emit. trigger_source distinguishes schedule / hook / listener / bridge; trigger_component_id is null on schedule and bridge rows; bridged_from carries 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 per assign, with variable_name, value_type, and typed value_number / value_text / value_boolean columns. trigger_component_id is null on scheduled assigns and on the t=0 initial-value rows.
If a shift change or kanban signal “didn’t seem to fire”, these tables are where you check.

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.