simulation-frame-merge (Transform Node)
The Simulation Frame Merge node takes a JSON payload containing long-format SimulationFrame rows (often one row per component per tick) and merges them into wide, CSV-friendly rows.
This is typically used after a hydrate step that emits SimulationFrame-like JSON where each record contains envelope fields plus a component_data object.
Configuration Schema
| Property | Type | Required | Description |
|---|---|---|---|
inputMode | "all" | "latest" | No | Input selection policy. all enumerates all available parent outputs (can duplicate rows if parents emit cumulative snapshots). latest uses only the most recent parent output. Defaults to all. |
root | string | No | JSON object key whose value is an array of long-format rows. Defaults to "simulation_frame". |
groupBy | array<string> | No | Fields to group by before merging long-format rows into one wide row. Defaults to ["simulation_tick","entity_id"]. |
Multi-origin capture (origin_uuid)
Long-format rows from more than one HEAT capture source can share the same simulation_tick and entity_id (for example client and server both observing the same vehicle). If you only group by simulation_tick and entity_id, those rows are merged into a single wide row. Envelope fields such as origin_uuid then use first non-empty wins (see implementation), so attribution to one source is easy to get wrong and timelines can look truncated when a downstream step keys off origin_uuid.
Recommendation: when multiple origin_uuid values appear in the hydrate output for the same entity, add origin_uuid to groupBy so each (simulation_tick, entity_id, origin_uuid) becomes its own merged row:
"groupBy": ["simulation_tick", "entity_id", "origin_uuid"]Tradeoff: In overlap regions you get up to one wide row per origin per tick (higher row count). Downstream nodes (for example tabular-remap) can then filter rows by origin_uuid if you need only one station (driver vs instructor) for a dashboard.
Note: origin_uuid identifies the capture client, not automatically a KB role (Driver vs Instructor). Dropping “instructor” rows still requires a deployment-specific mapping from role to origin_uuid (allowlist, or discardRowsExpr on known UUIDs). See dev-notes/origin-uuid-policy.md at the repository root for a policy template.
Behaviour
- Input selection (
inputMode):- Default (
all): the node enumerates all available upstream outputs and selects the first payload in that enumeration. latest: the node uses only the latest upstream output.- When to use
latest: when the parent node outputs cumulative snapshots over time. Usinglatestavoids reprocessing earlier partial snapshots and reduces duplicated CSV rows.
- Default (
- Note on multi-output parents:
inputMode: "all"enumerates outputs and some runtime paths return a list of payloads for a single parent (one per upstream output). In that case, this node selects the first payload in that list. - The node parses the upstream payload as JSON and looks for
root(an array). - Rows are grouped by
groupBy. For each group:- envelope fields are filled from the first non-empty value seen
component_datais flattened intocomponent_data_*keys and unioned into the merged row
- Output is a CSV string with headers inferred from the union of keys across merged rows.
Example configuration
Default (single origin or you accept merged envelopes):
{
"inputMode": "latest",
"root": "simulation_frame",
"groupBy": ["simulation_tick", "entity_id"]
}Multi-origin (separate wide rows per capture source):
{
"inputMode": "latest",
"root": "simulation_frame",
"groupBy": ["simulation_tick", "entity_id", "origin_uuid"]
}