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 has an Event Hooks section in its properties, plus an Event Listeners section once the model has at least one topic: they’re distinct first-class objects, not variants of the same thing. Topics and State Variables appear in the Variable and Topics sections of the Modeler’s Overview. Use a section’s Add State Variable / Add Topic row to create one quickly, or the header pencil (View and manage all) to open the Lookups dialog, which 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 Topics from the Topics section of the Overview or the Topics tab of the Lookups dialog. Once a Topic exists, components publish to it via an emit action and subscribe to it by adding an Event Listener whose Topic is set to 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 from the Variable section of the Overview or the State Variables tab of the Lookups dialog. Each has a Name (SCREAMING_SNAKE_CASE is enforced — CURRENT_SHIFT, not currentShift), a Type (Number, Text, or 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.
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 properties: click Add Hook, pick the Event, optionally add a Condition (optional), and configure one or more actions with Add Action.

Canonical Events per Component

Each event is only valid on specific component types and runs its expressions in a specific context. In results data the same events appear with an on_ prefix and underscores: entity created in the UI is on_entity_created in the event_lookup table. 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. buffer contents changed on a Buffer, station contents changed on a Station, 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 buffer 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 (optional) is only valid on Combiner entity consumed and Separator 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 the following (where it differs, the name in parentheses is how the action appears in results data):
  • assign: write to a state variable. Example: assign(CURRENT_SHIFT, "night") or assign(WIP_COUNT, WIP_COUNT + 1).
  • assign attribute (assign_attribute): write to an attribute of the entity the hook fired for, which keeps its id. The attribute must be declared on the entity type and the value must match its type; numbers are clipped to the declared bounds. Example: assign_attribute(visits, visits + 1) or assign_attribute(due_time, SIM_TIME + 43200). Valid only on single-entity events where the entity continues downstream: Source created, Buffer and Station entered / exited, Process started / completed, Router routed. Not valid in listeners. exited fires after the next component has accepted the entity, so that component’s own hooks read the old value, and a write after a Sink terminated the entity is dropped.
  • emit: publish a message to a topic. Example: emit("replenish").
  • pause: on a Source or Buffer, stop automatic output. On a Station or Process, stop running jobs while retaining their resource slots and remaining work; queued work waits. A paused Station also pauses its processes. Ask Dexter to set up named pause reasons when several causes can overlap.
  • resume: clear a pause. A Station or Process resumes only when every pause reason on it and its Station has cleared and its held resource slots are available.
  • release (release_entity): release entities on demand. Valid on Sources (creates new entities) and Buffers (releases queued entities). On a Buffer it only works when the Buffer’s Release Mode is Hold (manual release), where it is the only way entities leave; on a Buffer set to Push (auto-release) it silently does nothing, because entities already flow on their own, and a Push Buffer that also uses pause or resume is rejected. Quantity to release 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 expression (optional) (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 default to letting jobs finish. With the default ignore rule, a job already using a removed slot finishes first. Ask Dexter for the preempt rule to interrupt affected jobs immediately; they retain their slots and resume their remaining work when capacity returns. Use the same rule choice for shift-calendar capacity windows.

Per-Component Action Restrictions

Not every action is valid on every component: set capacity is valid only on Resource. release is valid only on Source and Buffer; pause and resume also support Station and Process. assign attribute needs one entity that continues downstream, and only on the events listed above. assign and emit are valid everywhere.

Conditions

Every event hook has a Condition (optional) 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 a component’s properties, a sibling to Event Hooks, with its own Add Listener button. The section appears once the model has at least one topic. Each listener has:
  • Topic (required): a declared topic
  • Condition (optional): boolean DSL expression
  • Actions: same action types as Event Hooks except assign attribute (a topic fires with no entity in scope), 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.

ModelNode and the Event System

Model Nodes can host Event Listeners but don’t support Event Hooks: a Model Node has no lifecycle events, so its properties show no Event Hooks section and validation rejects any hook on one. Listeners (topic-based) work fully and are the right tool for Model Node-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

Describe the behavior you want — “when the kitting buffer drops below 20, release 5 more from the Source” — and Dexter wires up the topics, state variables, hooks, and listeners with the same events and actions this page describes. Everything it builds shows up in the components’ Event Hooks and Event Listeners sections, where you can review and edit it.

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 / attribute_name (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.