Skip to main content

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:
  1. Tuning — adjust model parameters without hunting through expressions. Change BASE_PROCESSING_TIME from 60 to 75 and every relevant component follows.
  2. ReadabilitySHIFT_EFFICIENCY_FACTOR is easier to understand than 0.87 sprinkled across expressions.
  3. Non-technical access — operators and analysts who shouldn’t modify expressions can still tune the model by changing constant values.
Both Constants and Lookup Tables are factory-scoped — they live at the factory level and can be reused across models. But each model has to opt in by listing the constant or lookup table slug in its metadata.json constants[] / lookup_tables[] arrays. 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
  • Typenumber, text, or boolean
  • Value — the actual data
The type is text, not string. The schema enum is ["boolean", "text", "number"]. Writing "type": "string" in a constant definition fails validation. The UI Type dropdown labels the option Text.
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.
Reference a constant by its name in any expression:
BASE_CYCLE_TIME * SHIFT_EFFICIENCY_FACTOR

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 runtime assign action that constants don’t. A constant is the right tool for a parameter you tune between runs. 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.87 in 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 — changes propagate to every model in the factory that has the constant in its opt-in list. Each constant is stored at factory/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). 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:
LOOKUP(cycle_times, ENTITY_TYPE)

Providing a Fallback Value

LOOKUP accepts an optional default:= keyword argument that returns the supplied value when no row matches:
LOOKUP(yield_rates, ENTITY_TYPE, default:=0.95)
This is the canonical way to give a lookup a fallback. Always prefer 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 zero0 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.
The keyword-argument syntax uses := (walrus-style) — not = (which means equality). The same := is used for the filter:= and type:= keyword args on aggregation functions.

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:
LOOKUP(cycle_times_by_station, ENTITY_TYPE, station_name)
LOOKUP(cycle_times_by_station, ENTITY_TYPE, station_name, default:=180)
Each key column has its own type and an optional possible_keys allowlist (an array of values to validate against). 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.

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 at factory/lookup_tables/{slug}.json with a top-level value_type, an array of typed keys (the UI labels these Attributes), and rows.

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 IF expressions.
  • 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
It’s common to use both: a constant for a global factor, a lookup for per-type rates, combined in one expression.

Patterns

Processing time by product:
LOOKUP(cycle_times, ENTITY_TYPE) * BASE_PROCESSING_FACTOR
Conditional cost or surcharge with safe fallback:
IF(priority == "rush",
   LOOKUP(rush_surcharges, ENTITY_TYPE, default:=0),
   LOOKUP(standard_rates, ENTITY_TYPE, default:=0))
Tunable shift dynamics:
IF(SIM_TIME < SHIFT_1_END,
   BASE_RATE * SHIFT_1_EFFICIENCY,
   BASE_RATE * SHIFT_2_EFFICIENCY)

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.
  • Use default:= defensively. If a missing lookup row should be anything other than the type’s zero, default:= is shorter and clearer than an IF() wrapper around two LOOKUP calls.
  • Each model opts in. Adding a constant or lookup at the factory level is the first step. The model’s metadata.json constants[] / lookup_tables[] arrays decide whether that model sees it.