Skip to main content

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 / 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.
Boolean literals are case-insensitive: 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:
  1. Unary -
  2. Multiplication, division: *, /
  3. Addition, subtraction: +, -
  4. Comparison: <, <=, >, >=, =, ==, !=
  5. NOT
  6. AND
  7. 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.
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: Used in: Source arrival logic (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 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_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
  • Buffer release_entity quantity: how many held entities to release, computed over the buffer’s contents
  • Buffer on_buffer_contents_changed hook: fires with the buffer’s current contents in scope
  • Station on_station_contents_changed hook: fires with the station’s current contents 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).
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 time
  • SIM_DURATION: configured simulation duration
  • ENTITY_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’s id (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.

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 changeover
  • FROM_ENTITY_ATTRIBUTE / TO_ENTITY_ATTRIBUTE: attribute access on the outgoing and incoming entities
Use them to make changeover duration depend on what the resource is switching between — e.g., a paint booth whose cleanup takes longer when the color family changes.

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’s id (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 buffer
  • BUFFER_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 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:
The fallback fires whenever the id doesn’t resolve to a component of the right kind — a typo, a display name, or a valid id that belongs to a different component type (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:
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. 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:
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 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:
Or have the mean depend on a lookup table by entity type:
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.

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 value
  • POW(base, exp): base raised to exp
  • MOD(a, b): remainder of a divided by b
  • FLOOR(x): round down (toward −∞)
  • CEIL(x): round up (toward +∞)
  • ROUND(x, ndigits:=0): round to ndigits decimal places, half-to-even (banker’s rounding)
  • MIN_OF(a, b) / MAX_OF(a, b): smaller / larger of two numbers — scalar functions, distinct from the MIN / MAX aggregations
  • CLAMP(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 number
  • CONTAINS(text, substring): whether a string contains a substring (case-sensitive)
  • STARTS_WITH(text, prefix) / ENDS_WITH(text, suffix): boolean prefix/suffix tests
  • UPPER(text) / LOWER(text): case conversion
  • TO_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 with TO_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 optional default:= keyword argument:
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 (the seven surfaces listed in Expression Contexts). Aggregations operate over the set of entities in scope:
  • SUM(expr): sum → number
  • MEAN(expr): average → number
  • COUNT(expr): count of entities in scope → number (the canonical count idiom is COUNT(1), or COUNT(1, filter:=...))
  • MAX(expr) / MIN(expr): extremes — polymorphic: numeric input gives a number, text input compares lexicographically and gives text
  • MODE(expr): most common value — polymorphic; ties break to the first value seen
  • N_UNIQUE(expr): count of distinct values → number
  • ANY(expr) / ALL(expr): boolean input only → boolean
On an empty entity set, 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 true
  • type:=<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.
Example: a Combiner producing an assembly, setting output attributes from its inputs:

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:
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:
Backpressure: slow arrivals when downstream is congested, via an expression on the Source arrival rate:
Priority scoring: sort waiting entities by computed importance:
Assembly output: combine several inputs into one (Combiner, multi-entity context):

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".
  • NOT binds looser than comparisons. NOT qc_passed AND is_rush is (NOT qc_passed) AND is_rush, but NOT priority > 5 is NOT (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.