> ## Documentation Index
> Fetch the complete documentation index at: https://docs.prodexlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Validation

> The layer that checks your model before simulation runs: what it verifies and how to fix what it flags.

## 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](/product/ai-assistant).

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](/reference/batching#entity-identity-across-these-components))
* 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 `id`s 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_bound` ≤ `upper_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](/reference/entities#assignment-strategies) 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](#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](/reference/expressions): the [Expression Contexts](/reference/expressions#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](/reference/events)). 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.

<Note>
  **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.
</Note>

### 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](/product/ai-assistant) 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.
