> ## 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.

# Entities & Attributes

> Typed attributes that entities carry through the flow, the foundation that makes simulation models data-driven.

## Overview

An **Entity** is an item that flows through a simulation, a work order, a finished good, a pallet, a batch, a patient, a truck. Every simulation revolves around entities being created, routed, processed, and terminated.

What makes entities powerful in ProDex isn't the concept of "things flowing through a graph": it's that entities carry **typed attributes** with them, and expressions across the model can read and act on those attributes. An entity isn't just an anonymous token; it's a bundle of typed data that components inspect, route on, time-process differently, and aggregate. This is the mechanism that turns a static flow diagram into a model that responds to the specific work being done.

Entity types are one of the first things you define when building a model — see [Key concepts](/getting-started/key-concepts) for where they sit in the Factory > Models hierarchy, and the [Simulation overview](/product/simulation-overview) for the modeling workflow around them.

## Attribute Types

Each entity type defines a set of named attributes. The schema has five canonical types (`boolean`, `number`, `text`, `text_list`, `number_list`); the UI splits **Number** into Integer and Real, so the attribute-type dropdown shows six options. Under the hood that split isn't a sixth type: a Number attribute carries a `domain` field (`"int"` or `"real"`, defaulting to real), and the two dropdown entries surface it.

| Schema type     | UI label(s)                     | Description                                                                                                  | Example                                               |
| --------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- |
| **Boolean**     | Boolean                         | True/false flag                                                                                              | `is_rush`, `qc_passed`, `requires_rework`             |
| **Number**      | Number (Integer), Number (Real) | Integer or decimal via `domain`; optional `lower_bound` / `upper_bound`, plus an `exclude_zero` boolean flag | `weight`, `priority`, `complexity_factor`             |
| **Text**        | Text                            | Free-form string; optional `length` limit (an integer, minimum 1)                                            | Any text value                                        |
| **Text List**   | Text List                       | **Constrained choice from a fixed set of strings** (enum)                                                    | `product_class` where choices are `"A"`, `"B"`, `"C"` |
| **Number List** | Number List                     | **Constrained choice from a fixed set of numbers** (enum)                                                    | `size_tier` where choices are `1.0`, `2.5`, `5.0`     |

**The List types are enums, not lists.** A Text List attribute doesn't hold multiple strings: it holds *one* string, picked from a fixed set of allowed values declared on the entity type. Same for Number List: the attribute's value is one number chosen from a predefined set. Think of them as constrained categorical values, not arrays.

Use Text List or Number List when you want an attribute to be categorical and you want the platform to enforce valid values. Use Text or Number when the attribute is open-ended.

Attribute types are fixed on the entity type: you can't store a number in a Text attribute. The [expression language](/reference/expressions) respects these types when you reference attributes by name.

## How Attributes Flow

Attributes set on an entity persist with it through the entire flow. Once assigned, downstream components can read them without any explicit propagation.

* **Create / assign**: attributes are assigned any time a component produces a new entity. That's on **Sources** (creating initial entities), **Combiners** (output entity of a batch), **Separators** (output entities of a split), **Transformers** (output entity of a type change), and **material releases** on either a Source or a Buffer in a [schedule](/reference/schedules). In the model definition, Source/Combiner/Transformer carry a singular `attribute_assignment`; a Separator carries `attribute_assignments` (plural), keyed by output entity-type slug, because one split can emit several output types.
* **Read**: any downstream expression in a [single-entity context](/reference/expressions#single-entity-context) can reference the attribute by bare name (`priority`, `weight`).
* **Change identity**: a Transformer produces a *new entity of a different type* with its own attribute assignments. It doesn't mutate the input entity's attributes in place: it emits a different entity, typed differently, with its own attribute values.
* **Aggregate**: when entities are combined, the output entity's attributes are computed from the set of inputs using [aggregation functions](/reference/expressions#aggregation-functions) like `SUM(weight)` or `MAX(priority)` in the Combiner's multi-entity context.

<Warning>
  **Transformer attribute carryover is not automatic.** When a Transformer emits an output entity, none of the input entity's attributes are copied across by default. Every attribute you want on the output must be **explicitly assigned**, most commonly as a DSL expression referencing the input attribute by bare name. Skip an assignment and the value simply isn't carried — silent data loss. Separator outputs behave the same way.
</Warning>

The DSL doesn't require a prefix to read attributes: just use the bare name. `priority` and `product_class` are valid identifiers in any expression that has an entity in scope.

### Which context each assignment runs in

The single biggest authoring trap with attributes is forgetting **which [expression context](/reference/expressions#expression-contexts) an assignment evaluates in**:

| Where the assignment lives             | Context           | What its expressions can reference                                                                                                                                                                 |
| -------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Source `attribute_assignment`          | **No-entity**     | Constants, lookup tables, state variables, `SIM_TIME` — **not** entity attributes (there's no input entity yet)                                                                                    |
| Schedule material release `attributes` | **No-entity**     | Same as Source                                                                                                                                                                                     |
| Transformer / Separator assignments    | **Single-entity** | The *incoming* entity's attributes by bare name, plus everything above                                                                                                                             |
| Combiner `attribute_assignment`        | **Multi-entity**  | The batch of inputs via [aggregation functions](/reference/expressions#aggregation-functions); per-entity values like `ENTITY_TYPE` and `ENTITY_AGE` are only reachable inside an aggregation body |

Attribute names also can't collide with the DSL's **reserved keywords** — the validator rejects an entity type whose attribute shadows one.

## Assignment Strategies

Whenever a component assigns an attribute on a new entity (on a Source, Combiner, Separator, Transformer, or schedule material release), it uses one of **six** strategies. Not all six are valid for every attribute type; the dropdown filters to the strategies that work for the attribute you're configuring. (The per-type restrictions are enforced by the validator and surfaced by the UI — the schema itself is looser, so hand-authored JSON can express combinations that will then fail validation.)

| Strategy           | Purpose                                                                 | Valid attribute types                  |
| ------------------ | ----------------------------------------------------------------------- | -------------------------------------- |
| **Fixed**          | Assigns the same literal value to every entity                          | All types                              |
| **DSL Expression** | Evaluates a [DSL expression](/reference/expressions) at assignment time | Boolean, Number, Text (not list types) |
| **Round Robin**    | Cycles deterministically through the entity type's declared choices     | Boolean, Text List, Number List        |
| **Random Choice**  | Picks uniformly at random from the entity type's declared choices       | Boolean, Text List, Number List        |
| **Random**         | Samples from a [probability distribution](/reference/distributions)     | Number only                            |
| **Weighted**       | Picks from the entity type's choices with per-choice weights            | Boolean, Text List, Number List        |

<Note>
  **`Random Choice` and `Random` are different strategies.** `Random Choice` picks uniformly from a categorical choices array (boolean / text list / number list). `Random` samples from a probability distribution (number only). The dropdown filters them out based on attribute type, so you'll only see the ones that apply.
</Note>

### Strategy Details

**Fixed**: Same value on every entity. Useful for attributes that don't vary across items, or as a placeholder before you wire in real variability. On Text List / Number List attributes the fixed value must be one of the declared choices.

**DSL Expression**: The value is the result of a [DSL expression](/reference/expressions) evaluated when the entity is created. Use when the value depends on simulation state, other attributes, or the current time: `IF(SIM_TIME < SHIFT_1_END, "day", "night")` or `LOOKUP(priority_by_class, product_class)`. **Not valid for Text List or Number List**: for dynamic categorical selection use Weighted with DSL-expression weights, or restructure to Random Choice.

**Round Robin**: The first entity gets value 1, second gets value 2, third gets value 3, fourth cycles back to value 1. Useful for evenly distributing entities across categories without randomness. Round Robin carries no choices of its own — it cycles through the choices declared on the attribute.

**Random Choice**: Uniform random pick from the entity type's declared choices. Every choice has equal probability. For Boolean attributes the choice set is the implicit `{true, false}`.

**Random**: Samples from a distribution (normal, exponential, triangular, etc.). Useful for natural variability: entity weights drawn from a normal, service-time requirements drawn from triangular, or arrival intervals drawn from exponential.

**Weighted**: Pick from the entity type's choices with per-choice weights. Specify a weight for each option; relative weights determine selection probability. Weights can themselves be DSL expressions for dynamic distributions.

## Defining an Entity Type

Entity types are managed via the **Entities** button at the bottom of the Modeler's Library panel: it opens a modal listing every entity type in the factory with Name, Description, Unit, and Attrs columns. Each type has:

* A **name** and **description**
* A **unit** — a free-text label (default `"EA"`) that drives quantity labels in reporting and KPIs. It is a *label only*: ProDex does **not** perform dimensional conversion between units.
* A set of **typed attributes** (with `choices` arrays declared on Text List and Number List attributes, optional `lower_bound`/`upper_bound`/`exclude_zero` and `domain` on Number attributes, and optional `length` on Text)

Once defined, an entity type is available to every model in the factory. Sources, Transformers, BOM nodes, Combiners, and Separators that produce entities of that type share the same attribute schema: if you add an attribute to the type, every place that creates or modifies the entity can now set it.

Entity types live in `entities/{slug}.json` in the factory's data tree.

<Info>
  **The same entity types back your BOMs.** An entity type isn't simulation-only. A [BOM](/product/bom) references an entity type by its slug (`entity_id`), and its `applies_when` conditions read the type's attributes by name — the two features share one factory-scoped registry, the same `entities/{slug}.json` files. Define an entity type once and it's available to both the simulation model and the BOM/planning side; edit its attributes and both see the change.
</Info>

<Warning>
  **Renaming an entity slug or attribute propagates widely.** Entity slugs are referenced by `Source.entity_type`, Transformer `input_entity`/`output_entity`, Separator `input_entity` and output batch keys, `Combiner.output_entity`, schedule material releases (`entity_type_id`), [BOMs](/product/bom) (`entity_id`), and Configuration Templates. Attribute names appear in BOM `applies_when` conditions and DSL expressions across the model. Rename with a global search, not in isolation.
</Warning>

## Authoring Entity Types with Dexter

When [Dexter](/product/ai-assistant) builds entity types for you — typically after profiling your uploaded data — it authors them in code:

```python theme={null}
from prodex import simulation as sim

sim.entity(
    "Work Order",
    attributes={
        "priority": sim.number(domain="int", lower_bound=1, upper_bound=5),
        "product_class": sim.text_list(choices=["rush", "standard"]),
        "qc_passed": sim.boolean(),
    },
)
```

The constructors mirror the five schema types (`sim.boolean`, `sim.number`, `sim.text`, `sim.text_list`, `sim.number_list`). Ask Dexter to create or extend entity types from a spreadsheet and this is what runs underneath.

## Attributes in Results Data

Attributes are queryable after a run, but they live in a **dimension table**, not inline on every lifecycle row:

* Each `entity_lifecycle` row carries an `attributes_hash`. Join it to the **`entity_attribute`** table, which has one row per `(attributes_hash, attribute_name)`.
* `entity_attribute` columns: `attributes_hash`, `attribute_name`, `value_type` (`"number"`, `"text"`, or `"boolean"`), and typed value columns `value_number` / `value_text` / `value_boolean`. Booleans land in `value_boolean`, never `value_number`; Text List values land in `value_text` and Number List values in `value_number` (there are only three value buckets).
* Rows are written the **first time each distinct `attributes_hash` is seen** and never re-emitted; the table carries no entity identity of its own — the hash on `entity_lifecycle` is the only route back to entities.
* `entity_lifecycle.attributes_json` is populated only on material-release entry rows that carry per-release overrides; it's null everywhere else.

See [available dataframes](/product/results-and-analytics#querying-simulation-data) for the full catalog.

## Patterns

**Classification attribute driving routing.** A `product_class` Text List attribute set at the Source (one of `"rush"` or `"standard"`) drives a Router that sends rush items to the express line.

**Numeric attribute driving processing time.** A `complexity_factor` Number attribute (drawn from a distribution via the Random strategy) multiplies the base processing time on each Process.

**Type change in a Transformer.** A Transformer reassigns the entity to a new type `painted_part` (from `raw_part`), with explicit DSL-expression assignments that carry forward every attribute the downstream model cares about.

**Aggregation in a Combiner.** When three components are combined into an assembly, the output entity's `total_weight` is `SUM(weight)`, `priority` is `MAX(priority)`, and `qc_passed` is `ALL(qc_passed)`. The full aggregation set is `SUM`, `MEAN`, `COUNT`, `MAX`, `MIN`, `MODE`, `N_UNIQUE`, `ANY`, and `ALL` — `COUNT` and `N_UNIQUE` always return numbers, `ANY` and `ALL` are the boolean aggregations (over boolean attributes), and `MAX` / `MIN` / `MODE` work on numbers or text.

## Tips

* **Model the attributes you'll query.** If you care about "cycle time by product class" in your results, `product_class` has to be an attribute on the entity, otherwise the join isn't available in the [simulation dataframes](/product/results-and-analytics#querying-simulation-data).
* **Use Text List or Number List for categorical constraints.** They're enums: the platform enforces that the value is one of the declared choices. Prefer these over free-form Text when the attribute has a known finite set of valid values.
* **Prefer entity attributes over [state variables](/reference/events#state-variables) for per-entity data.** State variables are for facts about the *system*. Attributes are for facts about a specific entity.
* **For dynamic categorical selection, use Weighted with DSL-expression weights.** That's the only way to drive list-type attributes from expressions, since `DSL Expression` isn't valid for list types.
* **Explicit attribute carryover**: every output attribute on a Transformer or Separator must be assigned. There's no auto-copy from the input.
* **Stamp provenance on assignment values.** Choices lists, weights, and expressions inside attribute assignments accept the same `derived` / `stated` / `assumed` provenance stamps as everything else Dexter authors — unstamped values surface in an assumptions review.
* **`entity_id` doesn't survive transformation.** Transformers, Combiners, and Separators emit *new* entities with new ids. If you need to trace one unit end-to-end in results, carry a stable business identifier (order number, serial) as an attribute.
