Skip to main content

Overview

A Model Node is a component that embeds another simulation model inside the current one. Instead of duplicating components across many models, you define a sub-model once — a production cell, an assembly line, a department — and embed it wherever you need it. Changes to the sub-model propagate everywhere it’s used. This is the architecture for complex facilities: model a single cell rigorously, compose a department from copies of that cell, compose a facility from copies of the department. Without hierarchical modeling, a complex operation means thousands of components on a single canvas. With it, you get a clean decomposition that mirrors how your operation is actually organized.

When to Use Model Nodes

  • Repeated physical structure — your facility has five identical production cells. Model one, then embed it five times via Model Nodes.
  • Reusable templates — standard sub-processes (QC inspection, packaging, kitting) that appear across many models. Define once, reuse across models and even across factories via export/import.
  • Scale — once a model passes ~50-100 components, hierarchical decomposition becomes the only way to keep it navigable.
  • Team division — one person owns the line-level sub-model, another owns the facility-level composition. Model Nodes let them work in parallel.

How a Model Node Is Added

A Model Node is a reference to a separate model that already exists in the same factory. To add one to the current model:
  1. Open the Modeler’s left panel and stay on the Library tab.
  2. Scroll to the MODELS subsection at the bottom of the panel.
  3. Click + Import Model to open a picker listing the factory’s other models. Selecting one adds the model’s slug to the parent’s imported_models[] list and surfaces it as a draggable card in the MODELS subsection.
  4. Drag the card onto the canvas. A Model Node appears, referencing that sub-model.
The parent must list the sub-model in imported_models[] before a Model Node referencing it can validate. That list is the gate — without it, the ModelNode’s nested_model_id doesn’t resolve and the model fails validation. + Import Model manages this list automatically when you use it, but if you author the JSON by hand you must add the slug yourself.

Schema Fields

A ModelNode is a thin reference object — the bulk of what it does lives in the sub-model. Required fields are sparse; everything else is optional and defaults to empty.
FieldRequiredPurpose
idyesComponent id (used in qualified paths, schedule element_id, dataset columns)
nested_model_idyesSlug of the sub-model. Must appear in the parent’s imported_models[].
namenoDisplay name (defaults to id)
descriptionnoFree text
component_typenoConst "model_node" (auto-defaulted)
input_mappingsnoList of {external_connection_id, internal_component_id} pairs — wires parent connections to sub-model components
output_mappingsnoSame shape — wires sub-model components to parent connections
resource_mappingsnoPer-resource alias map; unmapped resources stay local
topic_mappingsnoPer-topic alias map; bridges parent ↔ sub-model bidirectionally
state_variable_mappingsnoPer-state-variable alias map; bridges read/write bidirectionally
event_listenersnoTopic-driven listeners on the ModelNode itself
event_hooksnoSchema accepts the field, but no canonical lifecycle events are defined for ModelNodes — see below
additionalProperties: false. No other fields are allowed.

Mapped Connections

input_mappings and output_mappings are the wiring layer: each entry pairs an external_connection_id (a connection on the parent canvas) with an internal_component_id (a component inside the sub-model). When the parent’s flow reaches an input mapping, entities flow into the named sub-model component; when an output mapping fires, entities cross back out. You can change the sub-model’s internal structure without breaking the parent — as long as the mappings still resolve to valid components inside, the parent model keeps working.

Shared Resources

A Resource can be shared between a parent and a sub-model by entering an explicit resource_mappings entry that aliases a parent resource id to the sub-model’s expected name. Unmapped resources stay local to the sub-model — if you don’t map it, the sub-model uses its own resource of that name. Use shared resources for operators or equipment that physically serve multiple cells.

Bridged Topics

Topics can be bridged across the parent/child boundary via topic_mappings. Bridging is always bidirectional — there’s no per-direction configuration, and the validator builds loop prevention into the topic graph automatically. An event emitted inside the sub-model on a bridged topic is received in the parent, and vice versa.

Aliased State Variables

State Variables can be aliased: a state variable declared at the parent level can be referenced from inside the sub-model under a different name via state_variable_mappings. The aliasing is bidirectional read/write — both sides see and write the same underlying value, with the validator enforcing matching types on both ends. This keeps sub-models decoupled (they reference their own internal names) while still letting them participate in parent-level state.

Constants and Lookup Tables Don’t Need Mapping

Constants and lookup tables are factory-scoped. Both the parent and sub-model reference them by slug independently — there’s no aliasing layer for these on a ModelNode. Each model has its own opt-in list (constants[] / lookup_tables[] in metadata.json); both can opt into the same factory-level constant without coordinating.

Qualified Component Paths

Components inside a Model Node are addressed by a qualified path using :: as the separator. A component with id lathe inside a ModelNode with id cell_a is referenced as cell_a::lathe anywhere outside its parent sub-model — the same form you’ll see in chart View source SQL, schedule element_ids, and dataset columns. All segments are component slugs, not display names. The :: separator is reserved — no single component id may contain it. This applies in three places:
  • Chart View source queries. Component identifiers in entity_lifecycle, process_activity, and other dataframes use the qualified form. Filtering by display name as it appears in the Modeler — say, component = 'Lathe' — misses every instance inside a Model Node. Use component = 'cell_a::lathe', or match with LIKE.
  • Schedule material releases. A scheduled material release to a Source inside a sub-model must name the Source with its qualified id: cell_a::raw_material_intake.
  • Cross-boundary references in DSL component-query functions — pass the qualified id when the component you’re querying lives in a sub-model.
Authoring hooks and listeners inside the sub-model uses local component ids — the qualified prefix is only needed when crossing the parent-child boundary. Sub-models stay reusable; they don’t need to know what name their parent gave them. For deeply nested structures, paths chain: plant_1::line_3::cell_2::lathe.

ModelNode Event System Support

ModelNodes can carry Event Listeners (topic-driven) and they work fully — listen for any topic in the parent or bridged into the sub-model, and run assign / emit / etc. like any other listener. Useful for ModelNode-level coordination that isn’t tied to a single internal component. ModelNodes also accept an event_hooks[] array in the schema, but no canonical lifecycle events are defined for ModelNodes — any hook you author has no valid event value and will fail validation. Stick to listeners.

Validation Recurses Across Boundaries

When you validate the parent model, validation recurses into every nested model referenced by a ModelNode. Errors inside a sub-model bubble up to the parent with a path prefix — for example:
“Nested model ‘cell_a’ (used by model node ‘fab_cell’): Process ‘lathe’ has expression error in processing_time…”
Fix the error in the sub-model file; the parent re-validates automatically.

Live Reference, Not Snapshot

A ModelNode references its sub-model by slug. Any change you make to the sub-model file is reflected immediately in every ModelNode that references it — there’s no snapshot pinning at the ModelNode level. If you need to freeze a specific revision of a sub-model, capture a Snapshot of the parent (which embeds the entire model registry at that moment).

Common Patterns

Cell template. A “production cell” sub-model is created once with canonical equipment. The facility model instantiates it five times via Model Nodes, each pointing at the same sub-model slug. Department composition. Build a “fabrication” model and an “assembly” model separately, then compose them in a facility model with connections between their outputs and inputs. Progressive detail. Start with a coarse facility model — a few ModelNodes, each representing a department as a black box. As you need more fidelity, replace a ModelNode’s sub-model reference with a more detailed one. The parent doesn’t change.

Tips

  • Treat sub-models as libraries. Once a sub-model is stable, avoid editing it casually — changes propagate to every instance.
  • Name inputs and outputs clearly. Mapping ids match by name; clear names prevent wiring mistakes.
  • Keep the top level thin. A well-organized facility model is mostly Model Nodes with a little glue between them. If your top level has hundreds of direct components, you’re losing the benefit.
  • All path segments are slugs. When in doubt about an id, check the schema or the dataset columns — display names with spaces don’t appear in :: paths.