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

# Combiners, Separators & Transformers

> The three components that reshape entity flow: batching multiple inputs into one, splitting one into many, and re-minting an entity mid-flow.

## Overview

Combiners, Separators, and Transformers are the three [Modeler components](/product/simulation-modeling#component-types) that reshape entity flow rather than simply routing or delaying entities. They're how you model assembly, disassembly, batching, kitting, packaging, and any operation where the entities going in aren't the same as the entities going out. (New to flow modeling? Start with the [Simulation Overview](/product/simulation-overview) for how these components fit into a model's flow graph.)

These components look simple (a box with inputs and outputs), but each has specific behavior worth understanding before you build an assembly or disassembly operation, especially around how they interact with [Station](/product/simulation-modeling#component-types) capacity, attribute carryover, entity identity, and the multi-entity DSL context.

## When a Combiner Is the Wrong Tool

Not every batch needs a Combiner. A Combiner **consumes** its inputs and mints a new output entity: each input's per-unit lifecycle ends at the batch seam, and downstream per-unit analysis (lead times, distinct-unit counts, yield) has to join across that seam. When items merely travel together for a stretch but stay individually meaningful, the recommended pattern is a **hold-mode Buffer with a conditional `release_entity` action** instead of a Combiner/Separator pair — released entities keep their identity end to end, so nothing has to be reassembled later.

<Tip>
  Reach for a Combiner (with or without a downstream Separator) when the batch itself is the thing being processed — a pallet or kit with its own routing, processing times, and attributes. Use a hold-and-release Buffer when you just need "wait until enough accumulate, then let them go together": a `release_entity` action on the Buffer's contents-changed hook fires once the accumulated count clears your threshold. See [Event System](/reference/events) for the `release_entity` action and its `quantity` and `filter` fields.
</Tip>

## Combiner

A **Combiner** merges multiple input entities into a single output entity. Use it to model assembly (parts → product), batching (individual items → pallet), kitting (components → kit), or any operation where several items become one.

### Input Batches

The Combiner's `input_batch` field has exactly **two modes**, picked from a binary Static / DSL Expression toggle on the config panel:

* **Static**: a `{entity_type: quantity}` map. `{tube: 1, tray: 1, label: 1}` means "wait until you have one of each before assembling." This is the canonical assembly shape: different counts of different input types feeding one output. Waiting entities are consumed **FIFO per input type** when the batch closes.
* **DSL Expression**: a boolean trigger expression evaluated each time an entity arrives. When the expression returns `true`, every accumulated entity is combined into one output. Use this when batch composition depends on runtime state.

The DSL mode has two schema fields: `input_entity_types` (an array of entity slugs the Combiner accepts for graph validation) and `batch_expr` (the boolean trigger). `batch_expr` runs in **multi-entity context** over the currently-accumulated entities, so you can write triggers like `COUNT(1) >= MIN_BATCH AND SUM(weight) >= LOAD_THRESHOLD`.

### Multi-Entity Context for Output Attributes

A Combiner output attribute assignment runs in **multi-entity context**: several input entities are in scope at once. Bare attribute references like `weight` are ambiguous (*which input's*?), so they must be wrapped in [aggregation functions](/reference/expressions#aggregation-functions):

```
# Output attribute: total_weight, sum of input weights
SUM(weight)

# Output attribute: priority, max across inputs
MAX(priority)

# Output attribute: is_rush, true if any input is rush
ANY(is_rush)

# Output attribute: qc_passed, true only if all inputs passed
ALL(qc_passed)
```

Output attribute assignments use the standard **6-strategy** dropdown (Fixed, DSL Expression, Round Robin, Random Choice, Random, Weighted), same as Sources, Transformers, and Separator outputs. The aggregation pattern shown above uses DSL Expression mode.

The full aggregation set available here is `SUM`, `MEAN`, `MAX`, `MIN` (polymorphic — text values compare in lexicographic order), `COUNT`, `ANY`, `ALL`, `MODE` (ties break to the first value seen), and `N_UNIQUE`. See [Aggregation Functions](/reference/expressions#aggregation-functions) for signatures and the `filter:=` / `type:=` keyword arguments.

<Warning>
  **An aggregation reads its attribute across every accumulated input.** If only some of the Combiner's input types define the attribute, the expression breaks — declare the attribute (with a sensible default) on every entity type the Combiner accepts.
</Warning>

### Combiner Events

A Combiner emits three lifecycle events (UI labels):

* `entity consumed`: fires for each input entity consumed (single-entity context, the input)
* `batch assembled`: fires when the batch closes (multi-entity context, all inputs)
* `entity created`: fires when the output entity is emitted (single-entity context, the output)

The `entity_type` filter on hooks is only valid on `entity consumed` (so you can react only to consumption of a specific input type).

### Partial Batches at Simulation End

A simulation can end with entities sitting in a Combiner that hasn't yet hit its batch threshold. During the run, those entities behave exactly as expected: the batch never closes, so no output entity is minted and nothing reaches a downstream Sink. What matters for your analysis is that they never contribute to *combined* throughput — the assembled entity that would have carried them forward is never created.

<Note>
  **A partial batch left open at simulation end never produces its output.** For short simulations with large batch sizes, this can materially affect combined-throughput figures. Pick a duration that lets the last expected batch close, or design the model so an unmet batch surfaces as an explicit event, rather than relying on where the leftover input entities land in the results.
</Note>

## Separator

A **Separator** splits one input entity into multiple output entities. Use it to model disassembly (kit → components), unbatching (pallet → individual items), or any "one in, many out" operation.

### Input Entity and Output Batch

The Separator config panel has two top-level controls:

* **Input Entity**: a single dropdown of entity types defined in the model. The Separator accepts only entities of this type.
* **Output Batch**: a `{output_entity_type: count_expression}` map. Each row is an entity type plus a **DSL expression** (a string) that returns the count of that type to emit per input. A row that says `component: "4"` means "emit 4 entities of type `component` per input." Variable counts can read input attributes: `component: "input_quantity"`.

Output count expressions run in **single-entity context** with the input entity's attributes in scope (`weight`, `priority`, etc.).

### Output Attributes

Each output type has its own attribute assignments in the schema's separate `attribute_assignments` field (note the plural: it's `attribute_assignments` on Separator, distinct from singular `attribute_assignment` on Combiner and Transformer).

`attribute_assignments` is a nested map: `{output_entity_type: {attribute_name: assignment}}`. Each assignment uses the 6-strategy dropdown.

Output attribute assignments run in **single-entity context with the input entity's attributes in scope**: references to `weight`, `priority`, etc. read the *input*, and the assignment produces values for the output entity being created. A common pattern: divide a quantity across outputs evenly (`weight / output_count`) or copy a classification from input to every output.

<Warning>
  **Separator outputs do not automatically inherit input attributes.** Every output attribute you care about must be explicitly assigned, most commonly with DSL Expression referencing the input attribute by name. Skipping an assignment gives that output attribute the type-default value (`0`, `""`, or `FALSE`), not the input's value.
</Warning>

### Separator Events

A Separator emits three lifecycle events:

* `entity consumed`: fires when the input is consumed (single-entity context, the input)
* `entity created`: fires for each output entity (single-entity context, the output). Only this event accepts the `entity_type` filter.
* `batch created`: fires once after all outputs are produced (multi-entity context, all outputs)

### Interaction with Station Capacity

Separator lineage in a station is often misunderstood. The correct model:

* **The input entity consumes one capacity slot** when it enters the station.
* **The Separator splits that input into several output entities.** The outputs are *descendants* of the input: they share the input's lineage.
* **The single slot is freed only when all descendants have exited the station.** If three outputs were produced, the slot stays occupied until all three of them leave.

The outputs don't each consume their own slot. They share the ancestor's slot via the lineage-tracking rule. So a station with capacity 5 can hold 5 Separator inputs at a time regardless of how many outputs each produces, but each input's slot stays occupied longer if it produces more outputs (because the "all descendants have exited" condition takes longer to satisfy).

<Note>
  **Station capacity keys are meaningful for entering entity types only.** Occupancy is tracked against the types that *enter* the station — internally-created output types share their ancestor's slot, so a capacity entry keyed on an internal type never comes into play. Key the capacity map on the types that actually enter.
</Note>

## Transformer

A **Transformer** consumes its input entity and emits a brand-new entity in its place — usually of a **different type**, though the output type may also be the same as the input's (see [the state-mutator pattern](#same-type-transformers-the-state-mutator-pattern) below). The entity count is unchanged (one in, one out), and the output is genuinely new: a fresh entity carrying only the attributes you explicitly assign.

Use a different-type Transformer to model operations where the thing being worked on becomes a *different thing* after processing: raw material → WIP, WIP → finished good, blank → painted, component → inspected-component.

### Attribute Assignment on the Output

A Transformer emits a new entity of a different type, with its own `attribute_assignment` (singular) map. Each assignment uses the 6-strategy dropdown: pick **DSL Expression** when the value is computed. Assignments run in **single-entity context** with the input entity's attributes accessible and can reference:

* **The input entity's attributes**: useful for carrying context forward
* **Simulation state**: `SIM_TIME` to stamp when the transformation happened
* **State variables and constants**: to apply shift- or operation-specific values

<Warning>
  **Transformer output attributes do not inherit from the input.** No attribute carries across automatically. Every output attribute you want must be explicitly assigned, typically as DSL Expression referencing the input attribute by name (`weight`, `priority`, `customer_id`). Missing assignments default to the type's zero value, which is silent data loss.
</Warning>

### Same-Type Transformers: the State-Mutator Pattern

The input and output entity types on a Transformer **may be the same type**. This is the canonical way to mutate an entity's attributes mid-flow. The classic example is a rework loop: a same-type Transformer whose output assignment sets

```
# Output attribute: rework_count (DSL Expression)
rework_count + 1
```

lets a downstream Router cap the loop with a condition like `rework_count < MAX_REWORK`. Two prerequisites make the pattern work:

* **Initialize the counter at the Source.** Every entity type that can enter the loop must assign the attribute (e.g., `rework_count = 0`) at its Source, so the first increment has a defined starting value.
* **Re-assign every attribute you want to keep.** A same-type Transformer is still a Transformer: the output is a new entity, and unassigned attributes fall back to type defaults, not the input's values.

### Transformer Events

A Transformer emits two lifecycle events:

* `entity consumed`: the input being consumed
* `entity created`: the output being emitted

Both run in single-entity context. There's no batch event: the Transformer is strictly 1-in, 1-out.

### When to Use a Transformer vs. a Process

If all you need is "the entity is delayed by X minutes, optionally holding a resource," use a [Process](/product/simulation-modeling#component-types). Use a Transformer when:

* The entity's downstream routing depends on its *type* (sent to different processes for different types)
* Results analysis groups by entity type (throughput by product, not aggregated)
* The entity conceptually becomes something different as part of the operation
* You need to mutate an attribute mid-flow (the same-type state-mutator pattern above)

A Transformer does not consume processing time by itself: if the transformation takes real time, pair it with a preceding Process.

## DSL Contexts on These Components

| Surface                                           | Context                                |
| ------------------------------------------------- | -------------------------------------- |
| Combiner `attribute_assignment` (output attrs)    | Multi-entity (all inputs)              |
| Combiner `batch_expr` (DSL trigger)               | Multi-entity (accumulated inputs)      |
| Combiner `on_entity_consumed` hook                | Single-entity (the input)              |
| Combiner `on_batch_assembled` hook                | Multi-entity (all inputs)              |
| Combiner `on_entity_created` hook                 | Single-entity (the output)             |
| Separator `output_batch` count expressions        | Single-entity (the input)              |
| Separator `attribute_assignments` (per output)    | Single-entity (the input)              |
| Separator `on_entity_consumed` hook               | Single-entity (the input)              |
| Separator `on_entity_created` hook                | Single-entity (the output, per output) |
| Separator `on_batch_created` hook                 | Multi-entity (all outputs)             |
| Transformer `attribute_assignment` (output attrs) | Single-entity (the input)              |
| Transformer hooks                                 | Single-entity                          |

Four of the platform's **seven multi-entity surfaces** live on Combiners and Separators: Combiner `attribute_assignment`, Combiner `batch_expr`, Combiner `on_batch_assembled`, and Separator `on_batch_created`. The other three sit elsewhere in the flow graph — Buffer `release_entity.quantity`, Buffer `on_buffer_contents_changed`, and Station `on_station_contents_changed`. Every other DSL surface is no-entity or single-entity. See [Expression Contexts](/reference/expressions#expression-contexts).

## Station Placement

Combiner, Separator, and Transformer are designed to live inside a [Station](/product/simulation-modeling#component-types), and placing each one in a Station is the recommended practice — it's what gives you the occupancy accounting and lineage-based capacity behavior described above. The `station_id` field is nullable in the schema, so treat station placement as strong guidance rather than a hard requirement enforced by validation.

<Tip>
  **When the containing Station isn't a real physical constraint, leave its capacity `null` (unlimited)** rather than inventing an arbitrarily large finite number. Finite capacity creates blocking behavior even when the number looks safely high.
</Tip>

## Entity Identity Across These Components

All three components **mint a new entity**: the output's engine-level identity is different from the input's. That's invisible when you only care about aggregate throughput, but it breaks per-unit tracing (lead time per work order, distinct-unit counts, yield by lot) unless you maintain a stable **business identity attribute** across every transformation:

1. **Declare** the attribute (`work_order`, `lot_number`, `serial_no`, …) on every entity type in the chain.
2. **Stamp** it at the Source that introduces the unit.
3. **Re-assign** it on every Combiner, Separator, and Transformer output the unit passes through — like every other output attribute, it doesn't carry over by itself.

Validation warns when a transformation output has no attribute assignment at all, which typically surfaces an accidentally dropped identity — but it isn't a dedicated "this identity survives end to end" check, so threading the attribute through every seam remains your responsibility. Dropping the identity at one seam is the single most common analysis-breaking mistake around these components.

<Warning>
  **`text_list` and `number_list` attributes cannot be assigned via DSL Expression.** Any identity or value you thread through a Combiner, Separator, or Transformer with an expression must live on a scalar `text` or `number` attribute — list types reject the DSL Expression strategy at validation. This bites people who reach for `text_list` because a value happens to be one of a few known strings.
</Warning>

## Event Hooks and Listeners

All three components carry both an **Event Hooks** section and a separate **Event Listeners** section in the config panel, exactly like every other flow component. Listeners react to topic emissions and always run no-entity context. See [Event System](/reference/events) for the full pattern.

## Patterns

**Assembly cell:** Sources → Buffers → Combiner inside a Station → Sink. Each Source feeds a different component type; the Combiner Static mode declares `{tube: 1, tray: 1, label: 1}`; the Station models the physical workspace constraint.

**Kit packaging:** Process (pick components) → Combiner (batch into kit) → Process (seal/label) → Sink. The Combiner produces the kit entity; downstream processes operate on kits.

**Disassembly for rework:** Router (send rework candidates here) → Separator (split the kit back into components) → individual component buffers. Each output's attributes are assigned explicitly (`rework_reason`, `original_kit_id`).

**Paint line:** Source (raw parts) → Process (paint) → Transformer (raw\_part → painted\_part) → Process (inspect) → Router (route by `qc_passed`). The type change after paint lets downstream processes specialize on `painted_part`.

## Tips

* **Combiners are where aggregation lives.** If you're trying to compute a sum or max across a group of entities anywhere else, you're probably fighting the model: restructure so the aggregation happens at a Combiner output.
* **Lineage-based capacity** is the reason stations with Combiners or Separators can behave counter-intuitively. Slots are consumed on entry and freed when all lineage descendants exit, not based on batch size or output count arithmetic.
* **Transformer over Process when type matters downstream.** Processes change state; Transformers change identity. Use each for its purpose.
* **Attribute carryover is never automatic on Transformers or Separators.** Every desired output attribute must be assigned explicitly.
* **Combiner DSL mode is for runtime-shaped batches.** If your batch size is constant, Static mode with a `{type: count}` map is clearer and faster.
* **Guard the identity attribute.** Re-assign `work_order` (or whatever your unit key is) on every transformation output; one missed seam severs per-unit traceability for everything downstream.
* **Batch with a hold-mode Buffer when items stay individual.** Combiner+Separator round-trips cost identity and add joins to analysis; a conditional `release_entity` on a Buffer doesn't.
