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 any distribution — processing times, arrivals, changeover durations, and the rest), 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 logic on a Source (an inter-arrival time, not a rate): can depend on simulation time, schedule, or state
- Routing conditions on a Router: branches based on entity attributes, resource availability, or queue state
- Routing weights on a Router: the relative weight of each branch when routing probabilistically, computed per entity
- Resource requirements on a Process: how many units of a resource are needed, conditional on the entity
- Changeover time on a Resource: how long a switchover takes, optionally depending on what the resource is switching between (see Changeover-Only Identifiers)
- 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/emitfires (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 inFUNCTION(args) form, arithmetic operators, comparison operators, and logical operators.
TRUE, True, true, FALSE, False, false all parse. Numeric literals cover integers, decimals, negatives, and scientific notation (1e-10).
Operator Precedence
Operator precedence, highest to lowest:- Unary
- - Multiplication, division:
*,/ - Addition, subtraction:
+,- - Comparison:
<,<=,>,>=,=,==,!= NOTANDOR
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.
NOT binds looser than comparisons, not tighter: NOT a = b parses as NOT (a = b), never (NOT a) = b. This is usually what you want, but it surprises people coming from languages where ! binds tightly. Parenthesize when in doubt.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:- Built-in state variables like
SIM_TIME,SIM_DURATION - Component query functions like
BUFFER_LEVEL(...),RESOURCE_AVAILABLE(...) - Constants by name
- State variables by name
SELF: when the expression is on an event hook or event listener (see SELF availability below)- Entity attributes are not available
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
entity created, process started), and similar places where one entity is clearly in scope.
Multi-Entity Context
Runs with multiple entities in scope. The seven 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_assembledhook: fires once per assembled batch with all inputs in scope - Separator
on_batch_createdhook: fires once per split with all outputs in scope - Buffer
release_entityquantity: how many held entities to release, computed over the buffer’s contents - Buffer
on_buffer_contents_changedhook: fires with the buffer’s current contents in scope - Station
on_station_contents_changedhook: fires with the station’s current contents in scope
priority are ambiguous (which entity’s?). You must wrap them in an aggregation function: MAX(priority) > 5, ANY(qc_passed).
ENTITY_TYPE and ENTITY_AGE follow the same rule in multi-entity context: they’re available only inside an aggregation body — MODE(ENTITY_TYPE), SUM(ENTITY_AGE), COUNT(1, filter:=ENTITY_TYPE == "tube") — never at the top level of the expression.Which Field Runs in Which Context
Built-in State Variables
SIM_TIME: current simulation timeSIM_DURATION: configured simulation durationENTITY_TYPE: entity type slug of the current entity (single-entity context, or inside aggregation bodies in multi-entity context). Returns the slug (e.g.,widget), not the display name (e.g.,Widget).ENTITY_AGE: how long the current entity has existed, i.e.SIM_TIME − creation_time(single-entity context, or inside aggregation bodies)SELF: the current component’sid(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)
Changeover-Only Identifiers
A Resource’s changeover time expression gets four identifiers that exist nowhere else in the DSL:FROM_ENTITY_TYPE/TO_ENTITY_TYPE: the entity type slugs on each side of the changeoverFROM_ENTITY_ATTRIBUTE/TO_ENTITY_ATTRIBUTE: attribute access on the outgoing and incoming entities
Component Query Functions
These functions read live simulation state for a specific component. The first argument is a text expression that evaluates to the component’sid (the schema id, not the display name) — a string literal is the common case, but an entity attribute holding an id, SELF, or a CONCAT(...) all work.
BUFFER_LEVEL(buffer_id): current number of entities in the named bufferBUFFER_CAPACITY(buffer_id): configured capacity of the named buffer (returns infinity for an unbounded buffer)RESOURCE_AVAILABLE(resource_id): currently available units of the named resourceRESOURCE_CAPACITY(resource_id): configured capacity of the named resourceSTATION_WIP(station_id [, entity_type]): work in progress at the named station; optional second argument filters to a specific entity type
Default Value
All component query functions accept an optionaldefault:= keyword argument that overrides the fallback:
BUFFER_LEVEL pointed at a resource). For STATION_WIP it also fires when the entity-type filter names a type with no capacity entry at that station. Without default:=, the fallback is numeric 0. Unlike LOOKUP, component queries do not return type-specific zero ("" or FALSE), only numbers.
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:assign action on event hooks, event listeners, or scheduled actions. They persist across entities and across the entire run.
State variables are declared in the model’s state_variables array, each with a name, a type (number, text, or boolean), and a matching initial_value. Names must match ^[A-Z][A-Z0-9_]*$ (SCREAMING_SNAKE_CASE) and must not collide — case-insensitively — with reserved DSL identifiers, constants, lookup tables, component ids, or entity type slugs (sim_time collides with SIM_TIME).
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:boolean, number, text, text_list, number_list. Despite the names, text_list and number_list are single-valued discrete enums (the entity holds one value from a declared set), not collections — and they reject the DSL Expression assignment strategy at validation. Reference the current entity’s attributes by their bare name in any expression that runs in a single-entity context:
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 writeNORMAL(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 eleven options (including lognormal_from_mean_cv, a distinct primitive parameterized by mean and coefficient of variation rather than the underlying normal’s parameters). 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.
The bridge in the other direction is the Fixed distribution: when a field expects a distribution but you want a fully computed value, pick Fixed and put the DSL expression in its Value parameter. That’s the documented way to make, say, a process time exactly equal to LOOKUP(cycle_times, ENTITY_TYPE) with no randomness on top.
For example, instead of a hardcoded mean of 60, you can flip the Mean field to expression mode and type:
10, or also be an expression. See Distributions for the full list of supported distribution types.
Function Catalog
The complete built-in function set, grouped the way you’ll reach for it.Control Flow
IF(condition, then, else): branch between two values. Both branches must return the same type.AND,OR,NOT: logical combinations (infix/prefix operators, not functions)- Comparison operators:
==,!=,<,<=,>,>=,=
Math
ABS(x): absolute valuePOW(base, exp):baseraised toexpMOD(a, b): remainder ofadivided bybFLOOR(x): round down (toward −∞)CEIL(x): round up (toward +∞)ROUND(x, ndigits:=0): round tondigitsdecimal places, half-to-even (banker’s rounding)MIN_OF(a, b)/MAX_OF(a, b): smaller / larger of two numbers — scalar functions, distinct from theMIN/MAXaggregationsCLAMP(value, lower, upper): constrain a value to a range (bound order doesn’t matter)- Standard arithmetic:
+,-,*,/
Strings
LEN(text): length of a string, as a numberCONTAINS(text, substring): whether a string contains a substring (case-sensitive)STARTS_WITH(text, prefix)/ENDS_WITH(text, suffix): boolean prefix/suffix testsUPPER(text)/LOWER(text): case conversionTO_TEXT(number): convert a number to text (whole numbers drop the trailing.0)CONCAT(text, text, ...): join two or more strings, in order. Numbers are not auto-coerced — wrap them withTO_TEXT:CONCAT("sku-", TO_TEXT(part_id))
Lookups
LOOKUP(table_name, key1, key2, ...): look up a value from a Lookup Table. Supply one key per dimension. Accepts an optionaldefault:=keyword argument:
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 (the seven surfaces listed in Expression Contexts). Aggregations operate over the set of entities in scope:SUM(expr): sum → numberMEAN(expr): average → numberCOUNT(expr): count of entities in scope → number (the canonical count idiom isCOUNT(1), orCOUNT(1, filter:=...))MAX(expr)/MIN(expr): extremes — polymorphic: numeric input gives a number, text input compares lexicographically and gives textMODE(expr): most common value — polymorphic; ties break to the first value seenN_UNIQUE(expr): count of distinct values → numberANY(expr)/ALL(expr): boolean input only → boolean
MAX, MIN, MODE, and MEAN return 0.0; SUM, COUNT, and N_UNIQUE return 0; ANY returns FALSE and ALL returns TRUE (vacuous truth).
Every aggregation accepts two optional keyword arguments:
filter:=<boolean expr>: only aggregate entities where the filter evaluates truetype:=<entity_type_slug>: only aggregate entities of the named type (unquoted slug)
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:=, so work around it with filter:=ENTITY_TYPE == "my-type" instead.Type Safety
The DSL is statically typed. Every expression is type-checked at model validation time, not at runtime — an expression error surfaces as a validation message with a field path, never as a mid-run stack trace. 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. What the checker enforces, per operator:
There is no implicit type coercion.
5 + "x" doesn’t quietly become "5x", it fails validation. Convert explicitly when you need to: CONCAT("sku-", TO_TEXT(part_id)).
Type checking happens at validation, but a few arithmetic edge cases are handled at run time rather than rejected: a division by zero or an expression that evaluates to
NaN resolves to 0 and records a run-level advisory instead of aborting the run. Your model keeps going; check the advisories if a computed value looks wrong.Common Patterns
Conditional routing: send each entity down the right path:10:
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 thedefault:=keyword arg if you need a different sentinel. - LOOKUP returns type-specific zero.
0,"", orFALSEdepending on the table’s value type. Wrap withdefault:=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
prioritynot available here,” the expression is running in a context that doesn’t have an entity in scope. See Expression Contexts. SELFvs. bare attribute names.SELFrefers to the component the expression is attached to (hooks and listeners only). Bare names refer to entity attributes. They don’t overlap.ENTITY_TYPEreturns the slug, not the display name. Compare against"widget", not"Widget".NOTbinds looser than comparisons.NOT qc_passed AND is_rushis(NOT qc_passed) AND is_rush, butNOT priority > 5isNOT (priority > 5). Parenthesize anything you’d have to think twice about.- Scalar vs. aggregate MIN/MAX.
MIN_OF(a, b)compares two numbers anywhere;MIN(expr)aggregates over entities and only works in multi-entity contexts. - 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.

