Skip to main content

Overview

Every time you change a model, ProDex runs a validation pass that checks the model against a comprehensive set of correctness rules. The result is the play button at the bottom of the canvas: green means the model is valid and ready to run; red means there’s something blocking simulation. Validation happens continuously while you build, which means you catch problems as you introduce them rather than discovering them hours later when a simulation fails.

The Red Play Button

When the play button at the bottom of the canvas is green, hovering it shows the tooltip “Run simulation.” Click it and the simulation runs. When it’s red, hovering opens a popover with:
  • A header counting the problems (e.g., “1 validation error” or “6 validation errors”).
  • The verbatim error text from the validator, naming the offending component and what’s wrong with it.
  • A Fix with Assistant button (with a sparkles icon) that hands the error to Dexter.
A real example of what you’ll see in the popover:
“Component ‘Halo Repress Halo Queue’ (type: BufferDefinition) must have exactly one outgoing standard connection, but has 2.”
Clicking Fix with Assistant drops that exact error into a Dexter conversation and asks it to propose a corrective change. Dexter reads the error, locates the component on the canvas, and suggests the edit, usually deleting the extra connection or splitting the buffer’s downstream into a Router. You review the proposed change before it’s applied. If you’d rather fix it yourself, the error message is specific enough to point you at the field, component, or expression to edit. The popover dismisses on un-hover, so reopen it if you need to re-read.

Errors vs. Warnings

Validation produces two severities, and the distinction shapes how you should read the results:
  • Errors block simulation. The play button stays red until every error is resolved.
  • Warnings are advisory: they never block a save and never block a run. They flag things that are structurally legal but probably not what you meant.
Checks also run in two layers. Fast structural checks run immediately as you edit: required-field shape, dangling connection endpoints, duplicate ids, naming rules (the :: restriction, reserved-word and slug collisions), and distribution-parameter sanity. The deep check runs against the saved model and covers everything semantic — connection cardinality and reachability, DSL typing, entity-flow compatibility, hooks against lifecycle events, topic acyclicity, schedule datetime handling, and nested-model recursion — so a change and its verdict are always sequenced through a save. If the deep check can’t reach the backend, you get the structural verdict alone, flagged as partial. Warnings worth knowing about (all advisory):
  • A Source, Sink, Process, or Buffer with no entity type assigned
  • A Combiner that consumes nothing (no input quantities)
  • A ModelNode with no nested model selected, or an empty entry in its topic or resource mappings
  • No attribute survives every transformation — no stable identity rides through your Combiners, Separators, and Transformers, so per-unit lineage and lead-time analysis degrade (see Entity Identity)
  • Probabilistic Router weights that don’t sum to 1 (the engine normalizes; the warning reports the resolved fractions)
  • A resource defined but required by no component
  • A schedule releasing large same-instant bursts (more than 10,000 entities at a single instant) that could trip the engine’s zero-delay livelock guard — stagger releases unless they truly co-occur

What Gets Validated

The validator’s rules fall into seven canonical categories: entity flow, connection cardinality, DSL typing, attribute assignment, hooks and actions, naming, and nested models & schedules. The sections below cover the most consequential rules, the ones that block real models day-to-day.

Configuration

Required top-level fields, file-format correctness, schedule presence (when required). A model without a metadata.json or with malformed JSON fails before any rule below it runs.

Naming and Identifiers

  • Component ids must be unique across all resources, stations, and components in the model
  • :: is reserved as the path separator, so no single component id may contain it
  • State variable names must not collide with component ids, entity slugs, or reserved DSL identifiers
  • Topic names must be unique
  • Entity attribute names must be unique per entity type
A collision fails fast with a specific error naming both ends of the conflict.

Connection Topology

Validation enforces strict cardinality on connections:
  • Source: 0 incoming, exactly 1 outgoing
  • Sink: 0 outgoing
  • Router, ModelNode: at least 1 outgoing
  • Process, Buffer, Combiner, Separator, Transformer: exactly 1 outgoing standard connection
  • You cannot draw a connection to or from a Station. Stations hold components; connections wire components, not containers.
Every non-Source component must also be reachable from a Source. Disconnected components (orphans, dead ends, components with the wrong number of outgoing edges) are caught here.

Entity Flow Consistency

Each component declares which entity types it accepts and emits. Validation checks that:
  • Every component’s expected input types are produced somewhere upstream
  • No component receives a type it can’t handle (the flow doesn’t carry a raw_material into a process expecting painted_part)
  • Combiner and Separator type expectations match the surrounding flow
This is how silent type mismatches get caught before they corrupt a run.

Entities

Attribute names unique per entity type; choices arrays valid for list types (text_list / number_list are single-valued discrete enums whose choices array is the allowed value set — not collections); lower_boundupper_bound on Number attributes; length non-negative on Text.

Components and Stations

Required fields per component type are enforced. station_id on Process, Combiner, Separator, and Transformer is nullable — placing these components in a Station is strongly recommended practice (it’s what drives occupancy accounting), not a validation requirement. Station capacity keys are meaningful for entity types that enter the station; entries keyed on internally-created output types never come into play, since outputs share their ancestor’s slot.

Resources

Every component reference must point at a real resource — a dangling reference is an error. A resource that’s defined but referenced by no component is legal (a normal mid-build state) and produces an advisory warning: “defined but required by nothing — it will not run as authored.”

Routers

A conditional router must at minimum declare a default route (default_outgoing_connection_id) for entities that match no rule; author it with the branch rules you actually want on top of that. Every connection a rule references must resolve to a real outgoing connection. Type-based routes map each expected input type to an outgoing connection and must cover the types that arrive. Probabilistic route weights don’t have to sum to 1 — the engine normalizes — but the validator warns and reports the resolved fractions so you can confirm they’re what you intended.

Transformers

Output entity types declared on the Transformer match the actual emitted types.

Entity-Generator Attribute Assignment

The strictest set of attribute-type restrictions:
  • dsl_expr is not valid for text_list or number_list attributes
  • round_robin and random_choice are valid only for boolean, text_list, and number_list
  • random (probability distribution sampling) is valid only for number
  • fixed and weighted are valid for any type
Mismatches fail at validation with a specific error pointing at the offending assignment. See Entities for the full compatibility matrix.

Distribution and Value Parameters

Numeric parameters are checked for legality, and the checks that matter most are the ones that would otherwise be silently corrected at run time — the model that runs would differ from the model you authored:
  • A triangular distribution’s mode outside its [lower, upper] range (otherwise clamped)
  • A fractional shape on an Erlang distribution (otherwise truncated to an integer)
  • A negative fixed value in a slot that must be non-negative — processing times, arrival logic, resource requirements, changeover times (otherwise clamped to zero)
This is why a distribution or a stray negative literal can turn the play button red: the validator would rather stop you than run a quietly different model. (It checks that parameters are legal, not that a distribution fits your data — see What Validation Does NOT Catch.)

DSL Expressions

The DSL is statically typed. Every expression is checked at validation time, not at runtime:
  • Boolean fields (conditions, gates) must produce booleans
  • Numeric fields must produce numbers
  • An aggregation function (SUM, MAX, ANY, etc.) outside a multi-entity context fails
  • An identifier not available in the expression’s context (e.g., an entity attribute in a no-entity context) fails
  • A referenced constant, lookup table, or state variable that doesn’t exist in the model’s opt-in list fails
See The Expression Language: the Expression Contexts section is the right starting point when the popover says an identifier isn’t available.

State Variables

Each state variable has a type (one of number, text, boolean) and a required initial_value matching that type. Mismatched types fail.

Topics

Topic names unique; the topic-trigger graph (edges from topic A to topic B exist when a listener on A emits B) must be acyclic. Topic cycles are caught here before they can cause runaway behavior at simulation time. Every declared topic must also be both emitted and listened to — an orphan topic (declared but never emitted, or emitted but never heard) fails validation.

Event Hooks and Listeners

Hooks reference valid lifecycle events for their host component type: process started on a Process is fine, process started on a Resource fails. Listener topic fields reference declared topics. Per-component action restrictions are enforced (see Event System). Two specific direct self-loops are forbidden: a Source’s on_entity_created hook running a release action, and a Buffer’s on_entity_exited hook running a release action, both fail validation.
Flow-graph cycles are explicitly allowed. Routing entities back through an upstream Process (rework loops) is a valid pattern. Only event-driven loops (topic acyclicity, direct self-loops) are forbidden.

Constants and Lookup Tables

Multi-key lookup tables require all LOOKUP() calls to pass the right number of keys with matching types. Key possible_keys allowlists (when declared) are enforced. Each model’s constants[] / lookup_tables[] opt-in arrays must reference real factory-level slugs.

Schedule

Schedules are validated together with the model they run against: material-release targets resolve against the model’s actual components, and hierarchical ids are resolved by the deep check.
  • Datetime fields (Start Time, and any release_time you set) must be timezone-aware ISO datetimes. A naive Start Time fails validation (“must include timezone information”): there is no implicit timezone fallback.
  • Every event timestamp must fall between Start Time and End Time (or Start Time + model Duration if End Time isn’t set)
  • element_id references must resolve to real components (including hierarchical parent::child paths)
  • Material releases target Sources or Buffers; releases to other component types fail
  • Scheduled actions are restricted to assign and emit: component-bound actions (pause, resume, set capacity, release) are not valid in schedules. Use a scheduled emit to a topic and let an Event Listener handle the side effect.
  • Releasing a very large batch at a single instant can trip the engine’s zero-delay livelock guard — the validator warns; stagger releases across nearby instants unless they genuinely co-occur.

Nested Models (Model Nodes)

Validation recurses into every nested model referenced by a ModelNode. Errors inside a sub-model bubble up with a path prefix: "Nested model 'cell_a' (used by model node 'fab_cell'): {error}". ModelNode nested_model_id must appear in the parent’s imported_models[] list. Mapping fields (input_mappings, output_mappings, resource_mappings, topic_mappings, state_variable_mappings) all resolve against real targets.

Simulation Duration

The model schema only requires a positive duration. The practical ceiling — one year (365 days) — is enforced platform-side rather than in the model JSON, so an over-long duration surfaces when you try to capture or run, not as a malformed file.

Sources Without Arrival Logic

A Source isn’t required to have an arrival_logic field. Three independent arrival modes can coexist on the same Source: stochastic arrivals from arrival_logic, scheduled releases from a schedule’s material_releases[], and event-driven releases from release actions on Event Hooks or Listeners. A Source with no arrival_logic, no schedule material releases, and no release actions has no way to produce entities: that’s a no-op model, not a validation error. The validator allows it because it’s a legitimate intermediate state during authoring.

What Validation Does NOT Catch

Validation catches structural and type-level correctness. A few classes of issue slip through:
  • Semantic correctness. A model that compiles and runs may still encode wrong assumptions about your operation. Validation can’t tell you a 30-second process should really be 30 minutes.
  • Provenance and assumptions. A parameter with no recorded source is perfectly valid. Validation doesn’t judge whether a value is stated (backed by data) or assumed — that’s what the model’s assumption tracking is for, and a clean validation pass with a dozen unsourced numbers is still an unfinished model.
  • Distribution fit quality. The validator checks a distribution’s parameters are legal, not that the distribution matches your data.
  • Resource starvation patterns. A model where a critical resource is held forever by a stuck entity validates fine but produces meaningless results. Read the run’s results to catch these.
  • Sensible Monte Carlo / experiment design. Validation runs on a single model; it doesn’t reason about whether your sweep of snapshots is statistically informative.
  • Performance. A valid model can still take a long time to run. Use Snapshots and short Durations to iterate fast.
The validator is a structural correctness gate, not a correctness proof.

Validation and Dexter

Validation applies to changes Dexter makes, too. When Dexter modifies your model, the same rules run: saving is gated by the structural checks, and the deep check delivers its verdict against the saved state. If Dexter tries to configure a component that would fail validation, the platform rejects the change and Dexter has to retry or ask you to clarify. This is one of the mechanisms that keeps Dexter from silently corrupting a working model. If Dexter is repeatedly failing to apply a change, check the validation errors yourself: often the fix is a small correction Dexter couldn’t infer (a missing constant, an attribute name that doesn’t exist on the entity type) and telling Dexter the specific fact unblocks it. The Fix with Assistant button is the same channel in reverse: it gives Dexter the exact error text so it doesn’t have to guess.

Tips

  • Fix structural errors first. Disconnected-component errors often hide expression errors that can’t run until the structure is sound.
  • Read the warnings, not just the errors. A green play button with warnings runs, but warnings like “no attribute survives every transformation” or “weights don’t sum to 1” are how models end up running incorrectly instead of not running.
  • The error messages are specific. If an error is confusing, search the message text in the docs: most validation errors map directly to concepts covered in the reference.
  • Validation is fast. You can make sweeping edits and let validation check them, so you don’t need to be surgical.
  • Red play button is your friend. It’s a forcing function that prevents you from wasting time on a simulation that was going to fail. Treat each red state as information, not an obstacle.