Overview
The Expression Language (or DSL) is how you make ProDex models dynamic. Wherever you’d expect to enter a fixed number — routing conditions, queue priorities, resource requirements, entity attribute values, event triggers, and the parameters of processing-time distributions — you can enter an expression instead. Expressions reference simulation state, entity attributes, constants, lookup tables, and state variables, so your model responds to the data flowing through it rather than behaving identically for every entity.
A user who only enters fixed numbers is using a small fraction of what the Modeler can do. Learning the expression language is what turns a static flow diagram into a living model of your operations.
Where Expressions Appear
Most numeric and logical fields in a component configuration accept either a literal value or an expression. Common places:
- Arrival rate on a Source — can depend on simulation time, schedule, or state
- Routing conditions on a Router — branches based on entity attributes, resource availability, or queue state
- Resource requirements on a Process — how many units of a resource are needed, conditional on the entity
- Queue priority on a Buffer — sorts waiting entities by a computed score
- Resource allocation priority — picks among competing demands when capacity is constrained
- Event Hook condition and Action arguments — see Event Hooks
- Event Listener condition and Action arguments — see Event Listeners
- Scheduled action condition — gate whether a scheduled
assign / emit fires (see Schedules)
- Entity attribute assignments on a Source, Transformer, Combiner, or Separator output — compute initial or updated attribute values
- Combiner
batch_expr — the boolean trigger that closes a dynamic-size batch
- Separator output count — DSL expression returning the number of outputs per input
- Distribution parameters — every numeric field that accepts a distribution has a
</> toggle that switches the parameter input into expression mode. See Distributions vs. Expressions below.
Syntax Basics
Expressions follow an Excel-like syntax: numeric literals, string literals in single OR double quotes, function calls in FUNCTION(args) form, arithmetic operators, comparison operators, and logical operators.
5
"TypeA"
'TypeB'
FUNCTION(arg1, arg2)
a + b * c
IF(condition, then_value, else_value)
priority > 5 AND qc_passed
Boolean literals are case-insensitive — TRUE, True, true, FALSE, False, false all parse.
Operator Precedence
Expressions follow standard operator precedence (highest to lowest):
NOT, unary -
- Multiplication, division —
*, /
- Addition, subtraction —
+, -
- Comparison —
<, <=, >, >=, =, ==, !=
AND
OR
AND, OR, and NOT are infix and prefix operators, not functions — write a AND b, a OR b, NOT a, never AND(a, b). Both = and == are accepted as equality operators. Use parentheses for explicit grouping when you want to override precedence.
Expression Contexts
The context an expression runs in determines which identifiers are available to reference. Getting context wrong produces validation errors like “identifier not available here” that are confusing without understanding the model. There are three contexts.
No-Entity Context
Runs without a specific entity in scope. Available identifiers:
Used in: Source arrival rates (before any entity exists), Event Listener conditions and actions (listeners always run no-entity), Resource events, scheduled actions, and global state checks.
Single-Entity Context
Runs with one specific entity in scope. Everything from no-entity context, plus:
- Entity attributes by bare name —
priority, product_class, weight
ENTITY_TYPE, ENTITY_AGE
Used in: Process processing time, Router conditions, Transformer attribute assignments, per-entity Event Hooks (e.g., entity created, process started), and similar places where one entity is clearly in scope.
Multi-Entity Context
Runs with multiple entities in scope. The four multi-entity surfaces are:
- Combiner attribute assignment — output entity attributes derived from inputs
- Combiner
batch_expr — the boolean trigger that closes a dynamic batch
- Combiner
on_batch_assembled hook — fires once per assembled batch with all inputs in scope
- Separator
on_batch_created hook — fires once per split with all outputs in scope
In any multi-entity context, bare attribute references like priority are ambiguous (which entity’s?). You must wrap them in an aggregation function: MAX(priority) > 5, ANY(qc_passed).
Built-in State Variables
SIM_TIME — current simulation time
SIM_DURATION — configured simulation duration
ENTITY_TYPE — entity type slug of the current entity (single-entity context only). Returns the slug (e.g., widget), not the display name (e.g., Widget).
ENTITY_AGE — time the current entity has been in the system (single-entity context only)
SELF — the current component (see SELF availability)
SELF Availability
SELF is only valid inside event hook and event listener expressions. It is not available in:
- Scheduled actions
- Component configuration fields outside of hooks/listeners (processing_time, arrival_logic, routing rules, queue priority, resource allocation priority, attribute assignments)
If you need to reference a specific component from outside a hook, use that component’s name explicitly inside a component query function.
Component Query Functions
These functions read live simulation state for a specific component. They take the component’s id field (the schema id, not the display name) as the first argument.
BUFFER_LEVEL(buffer_id) — current number of entities in the named buffer
BUFFER_CAPACITY(buffer_id) — configured capacity of the named buffer
RESOURCE_AVAILABLE(resource_id) — currently available units of the named resource
RESOURCE_CAPACITY(resource_id) — configured capacity of the named resource
STATION_WIP(station_id [, entity_type]) — work in progress at the named station; optional second argument filters to a specific entity type
Component query functions take the component id, not the display name shown in the Modeler. If your buffer’s display name is “WIP Buffer” and its id is wip-buffer, you must write BUFFER_LEVEL("wip-buffer"). Passing the display name silently returns 0 and your model behaves as if the buffer is always empty.
Default Value
All component query functions accept an optional default:= keyword argument that overrides the fallback when the component isn’t found:
BUFFER_LEVEL("wip-buffer", default:=-1)
Without default:=, missing components return numeric 0. Unlike LOOKUP, component queries do not return type-specific zero ("" or FALSE) — only 0.
Custom State Variables
State variables defined on the model are readable by their bare SCREAMING_SNAKE_CASE name in any DSL context — no-entity, single-entity, or multi-entity:
current_shift_efficiency
UNITS_COMPLETED + 1
IF(SHIFT_MODE == "night", base_time * 1.2, base_time)
State variables are read directly in expressions and written via the assign action on event hooks, event listeners, or scheduled actions. They persist across entities and across the entire run.
For the distinction between state variables (model-level, mutable) and entity attributes (per-entity, move with the entity), see Entity Attributes below.
Entity Attributes
Entities carry typed attributes through the flow — booleans, numbers, text, text lists, number lists. Reference the current entity’s attributes by their bare name in any expression that runs in a single-entity context:
priority
product_class
weight
No self. prefix — the entity in scope is implicit. Expressions reference attributes directly by the name defined on the entity type.
In multi-entity contexts, bare attribute names refer to the set of entities and must be wrapped in an aggregation function like SUM(weight) or MAX(priority). See Aggregation Functions.
Entity attributes are set on a Source, can be modified by a Transformer, and can be read from any expression downstream. They persist with the entity through the entire flow. See Entities for the full attribute type system and assignment strategies.
Distributions vs. Expressions
A common confusion: distributions are not DSL functions. You can’t write NORMAL(60, 10) as an expression. Distributions are picked from a dropdown on every numeric field that supports them — Normal, Uniform, Triangular, Beta, and the rest of the ten options. Selecting one reveals the parameter fields beneath the picker.
Each parameter field has a </> toggle that switches it into expression mode — that’s where the DSL comes in. The distribution shape stays static; the parameter values become dynamic.
For example, instead of a hardcoded mean of 60, you can flip the Mean field to expression mode and type:
Or have the mean depend on a lookup table by entity type:
LOOKUP(cycle_times_by_product, ENTITY_TYPE) * complexity_factor
The Standard Deviation field can stay as a literal 10, or also be an expression. See Distributions for the full list of supported distribution types.
Common Functions
Control Flow
IF(condition, then, else) — branch between two values
AND, OR, NOT — logical combinations (infix/prefix operators, not functions)
- Comparison operators:
==, !=, <, <=, >, >=, =
Math
ABS(x) — absolute value
POW(x, y) — x raised to the y
MOD(x, y) — remainder of x divided by y
- Standard arithmetic:
+, -, *, /
Strings
CONTAINS(text, substring) — whether a string contains a substring (case-sensitive)
LEN(text) — length of a string, as a number
Lookups
LOOKUP(table_name, key1, key2, ...) — look up a value from a Lookup Table. Supply one key per dimension. Accepts an optional default:= keyword argument:
LOOKUP(setup_times, product_class, default:=300)
Without default:=, missing rows return type-specific zero (0 for numeric tables, "" for text, FALSE for boolean). See Constants & Lookup Tables for the full schema.
Aggregation Functions
Valid only in multi-entity contexts (Combiner attribute assignment, Combiner batch_expr, Combiner on_batch_assembled, Separator on_batch_created). Aggregations operate over the set of entities in scope:
SUM(expr) — sum
MEAN(expr) — average
COUNT(expr) — count of entities in scope (canonically COUNT(1))
MAX(expr) — maximum value
MIN(expr) — minimum value
MODE(expr) — most common value
N_UNIQUE(expr) — count of distinct values
ANY(expr) — true if the expression is true for any entity
ALL(expr) — true if the expression is true for every entity
Every aggregation accepts two optional keyword arguments:
filter:=<boolean expr> — only aggregate entities where the filter evaluates true
type:=<entity_type_slug> — only aggregate entities of the named type (unquoted slug)
SUM(weight, filter:=qc_passed)
COUNT(1, type:=tube)
MAX(priority, filter:=is_rush)
type:= takes an unquoted slug, and hyphenated slugs are not supported in this position. A type with a hyphen in its slug can’t be filtered with type:= — work around it with filter:=ENTITY_TYPE == "my-type" instead.
Example — a Combiner producing an assembly, setting output attributes from its inputs:
# Output attribute: total_weight
SUM(weight)
# Output attribute: priority
MAX(priority)
# Output condition: emit only if every input passed QC
ALL(qc_passed)
Type Safety
The DSL is statically typed. Every expression is type-checked at model validation time, not at runtime. A condition field expects a boolean; an attribute assignment for a Number attribute must produce a number; a quantity expression must produce an integer. Mixing types fails validation with a specific error pointing at the offending expression — see Validation.
There is no implicit type coercion. 5 + "x" doesn’t quietly become "5x" — it fails validation. Convert explicitly when you need to.
Common Patterns
Conditional routing — send each entity down the right path:
IF(product_class = "rush", "express_line", "standard_line")
Attribute-driven distribution parameter — flip the Mean field of a Normal distribution into expression mode so it adapts to each entity, while Standard Deviation stays at a fixed 10:
LOOKUP(cycle_times_by_product, ENTITY_TYPE) * complexity_factor
Backpressure — slow arrivals when downstream is congested, via an expression on the Source arrival rate:
IF(BUFFER_LEVEL("wip-buffer") > 100, 0, 1)
Priority scoring — sort waiting entities by computed importance:
due_priority * 0.7 + customer_tier * 0.3
Assembly output — combine several inputs into one (Combiner, multi-entity context):
# Assembled entity's weight is the sum of its inputs
SUM(weight)
# Emit only if all inputs passed QC
ALL(qc_passed)
Tips and Gotchas
- Expressions are evaluated on every entity, every time. Keep them simple where possible — complex expressions in high-throughput paths add up.
- Component queries return numeric zero. A missing component name produces
0, not null. Use the default:= keyword arg if you need a different sentinel.
- LOOKUP returns type-specific zero.
0, "", or FALSE depending on the table’s value type. Wrap with default:= for a different fallback.
- Entity attributes are not State Variables. Attributes belong to an entity and move with it. State Variables belong to the model and persist across entities.
- Context matters. If you hit a validation error like “identifier
priority not available here,” the expression is running in a context that doesn’t have an entity in scope. See Expression Contexts.
SELF vs. bare attribute names. SELF refers to the component the expression is attached to (hooks and listeners only). Bare names refer to entity attributes. They don’t overlap.
ENTITY_TYPE returns the slug, not the display name. Compare against "widget", not "Widget".
- Prefer Constants and Lookup Tables over hardcoded values. Change a constant in one place; you can’t easily find-and-replace across dozens of expressions.