Overview
Constants and Lookup Tables are how you separate a model’s parameters from its logic. Instead of hardcoding values inside expressions, you name them, store them in one place, and reference them from anywhere. Change the named value once, and every expression that references it updates automatically. This matters for three reasons:- Tuning: adjust model parameters without hunting through expressions. Change
BASE_PROCESSING_TIMEfrom 60 to 75 and every relevant component follows. - Readability:
SHIFT_EFFICIENCY_FACTORis easier to understand than0.87sprinkled across expressions. - Non-technical access: operators and analysts who shouldn’t modify expressions can still tune the model by changing constant values.
constants / lookup_tables arrays of its model.json. A constant that exists in the factory but isn’t in the model’s opt-in list is invisible to that model.
Constants
A Constant is a named value with a specific type:number, text, or boolean. Each constant has:
- Name: how expressions reference it. Names use SCREAMING_SNAKE_CASE (e.g.,
BASE_CYCLE_TIME) - Description: what it represents and what units
- Type:
number,text, orboolean - Value: the actual data
Names auto-uppercase as you type. The constant-name input transforms input client-side to SCREAMING_SNAKE_CASE: typing
lowercase_name saves as LOWERCASE_NAME. Underscores you type are preserved; spaces are not added for you. Reference the name as it ends up after the transform.Constants vs. State Variables
Constants are static for the duration of a run: they cannot be reassigned. To mutate a value during simulation, use a State Variable instead. State variables expose a runtimeassign action that constants don’t.
The scope differs too: constants are factory-scoped and shared across every model that opts in, while state variables are model-scoped. A constant is the right tool for a parameter you tune between runs or share between models. A state variable is the right tool for a value the simulation itself changes: current shift, rolling WIP counter, demand signal.
When to Use a Constant
- Any value that appears in multiple places. If you find yourself typing
0.87in several expressions, extract it to a constant. - Any value you might want to change between runs. Edit the constant, capture a new Snapshot, and the snapshot freezes that value for downstream comparison.
- Any value a non-technical stakeholder might need to adjust. Analysts can tune a constant without knowing the expression language.
Experiments don’t auto-sweep constants. An Experiment compares Snapshots, not parameter values directly. To sweep a constant across an experiment, change its value, capture a snapshot, change it again, capture another snapshot, then add both snapshots to the experiment.
Managing Constants
Constants live in the Lookups modal in the Modeler’s Library panel: click the Lookups button at the bottom of the Library and open the Constants tab. The same modal also hosts Lookup Tables, State Variables, and Topics, each on its own tab. Add, edit, or delete constants there, and changes propagate to every model in the factory that has the constant in its opt-in list. Each constant is stored atfactory/constants/{slug}.json.
Lookup Tables
A Lookup Table is a named, typed-key table queryable in expressions. Each table has a value type (number, text, or boolean) and one or more keys (called “Attributes” in the UI — these are the table’s key columns, unrelated to entity attributes). Keys are themselves typed (each can be boolean, text, or number), so a multi-dimensional lookup can mix key types.
Query a table with LOOKUP:
Providing a Fallback Value
LOOKUP accepts an optional default:= keyword argument that returns the supplied value when no row matches:
default:= over wrapping in IF(): it’s a single expression with no double-evaluation of the same lookup.
Without
default:=, missing rows return type-specific zero: 0 for a numeric table, "" (empty string) for a text table, FALSE for a boolean table. That zero is silent and easily mistaken for a real result. Use default:= whenever the difference between “no row matched” and “value is exactly zero” matters.:=, not = (which means equality in the DSL). Despite the visual resemblance to Python’s walrus operator, := here is simply a named-argument marker, not an assignment. The same := marks the filter:= and type:= keyword args on aggregation functions and ndigits:= on ROUND.
Multi-Dimensional Lookups
A Lookup Table can be configured with more than one key column, so a single table can capture data that varies across two or more axes at once, like “processing time by (product type, station)” or “yield rate by (shift, material class).” Query it with one positional argument per key column, in the order the table defines them:possible_keys allowlist — the enumerated set of values that column accepts. possible_keys is load-bearing, not decoration: the platform validates every table entry against it at build time, and the UI’s key dropdowns are populated from it. Design a lookup table by enumerating each key’s domain up front.
Multi-key LOOKUP calls match rows where all key columns equal the values you pass in.
One argument per key column. There’s no wildcard or partial-match syntax: always pass exactly as many keys as the table defines.
Composite keys via CONCAT. When a natural key is a compound string, build it in the expression rather than adding a column per fragment:
Managing Lookup Tables
Lookup Tables are managed in the same Lookups modal as Constants: open it from the Library panel and switch to the Lookup Tables tab. Each table is stored atfactory/lookup_tables/{slug}.json with:
type— the value type (number,text, orboolean)key_definitions— the typed key columns (the UI labels these Attributes), each carrying aname, atype, and itspossible_keysallowlistentries— the rows, each{"keys": [...], "value": ...}with one key per column in definition orderdimension— the key-column count, derived fromkey_definitions
keys array that doesn’t match the column count, a key outside its column’s possible_keys, or a value that doesn’t match the declared type is rejected before the table is accepted — you won’t discover a malformed row at simulation time.
When to Use a Lookup Table
- Values that vary by a categorical attribute. Processing time by product type, yield rate by material, priority weight by customer tier, all natural lookups.
- Routing and proportional splits. Different entities route differently; store the routing rules in a table rather than deeply nested
IFexpressions. - Data-driven configuration. When the “configuration” of a model is really a set of numbers tied to categories, a lookup table is clearer than encoding the same information across many fields.
Constants vs. Lookup Tables: Which to Use?
- Single value used in many places → Constant
- Value that depends on a categorical attribute → Lookup Table
- Needs to be swept across snapshots in an experiment → Constant (one value per snapshot)
- Large reference dataset → Lookup Table
Patterns
Processing time by product:Tips
- DSL arithmetic is type-strict. Numbers add to numbers; text doesn’t quietly coerce. Catch type mismatches at validation rather than relying on silent conversions.
- The value type must match the context that reads it. A
booleanconstant in a numeric expression, or atextlookup value feeding a numeric slot, fails validation with a type error — a common trip point after refactoring a table from number to text values. - Use
default:=defensively. If a missing lookup row should be anything other than the type’s zero,default:=is shorter and clearer than anIF()wrapper around twoLOOKUPcalls. - Each model opts in. Adding a constant or lookup at the factory level is the first step. The model’s top-level
constants/lookup_tablesarrays decide whether that model sees it.

