---
title: "Composed props building organic and complex objects from primitives in one prefab"
description: "A prefab is not one entity. Its body is a whole entity SUBTREE: the `entities` array of `project_create_prefab` (`project.prefab.create`) may carry many entities, and `parentLocalId` on each one wires the hierarchy. That is the whole craft here: a tree is a trunk with branch children and leaf-cluster grandchildren; a bench is a frame with a seat slab, a backrest, and four legs; a rock cluster is o"
engineVersion: v1.0.234
date: 2026-09-28
license: "(c) Gessa, proprietary. Cite with attribution to https://gessa.ai/docs/. Terms: https://gessa.ai/terms/."
canonical: https://gessa.ai/docs/knowledge/playbooks/composed-props-from-primitives/
---

# Composed props building organic and complex objects from primitives

## The idea a prefab is a subtree

A prefab is not one entity. Its body is a whole entity SUBTREE: the `entities`
array of `project_create_prefab` (`project.prefab.create`) may carry many
entities, and `parentLocalId` on each one wires the hierarchy. That is the whole
craft here: a tree is a trunk with branch children and leaf-cluster
grandchildren; a bench is a frame with a seat slab, a backrest, and four legs; a
rock cluster is one base boulder with satellite stones parented to it. You build
the complex object ONCE, as one atomic prefab, then drop instances of it.

This is the same tool asset-to-instances teaches for the single-root case; this
playbook is about using its FULL power, the multi-entity body.

## The fields that carry the hierarchy

Each item in `entities` is a `PrefabEntityDto`. The load-bearing fields:

- `localId` (required): a stable id unique WITHIN the prefab. Children point at it.
- `key` (required): the graph key (lowercase, `/^[a-z0-9][a-z0-9._-]*$/`).
- `name` (required): the human label shown in the outliner.
- `parentLocalId` (optional): the `localId` of this part's parent. Omit or null
  on the root. A child's `TransformComponent` is then LOCAL to its parent, so you
  pose branches relative to the trunk, not in world space.
- `siblingOrder` (optional number): deterministic order among siblings. Set it so
  the outliner and any ordered logic read the same every rebuild.
- `components` (default `{}`): the part's components, keyed by a free lowercase
  slot id with the `type` inside each value, exactly as `project_create_entity`
  takes them, e.g. `{ transform: { type: "TransformComponent", position: {...} },
  render: { type: "RenderableComponent", shape: "capsule", visible: true } }`.
  Pick `shape` from the RenderableComponent shape enum (see
  docs/spec/generated/component-types.md). There is no cylinder or cone entity
  shape, so a trunk, a post or a leg is a capsule or a box; a true cylinder is a
  part inside a model (see model-authoring-parametric-csg).
- `scripts` (default `[]`), `tags` (default `[]`): as on any entity.
- `prefabInstance` (optional): make this part an instance of ANOTHER prefab (see
  "Nesting a prefab as a part" below).

Every entity also carries the standard content facets `identity`, `editor`, and
`runtime`. They are all-defaulted, so pass `{}` for each to take defaults (an
empty object is valid and fills the schema). The prefab as a whole needs a
`key`, a `name`, and a `rootLocalId` that names the root entity's `localId`.

One note on the examples below: `RenderableComponent.materialRef` resolves to a
material ASSET id (a uuid), not a symbolic name. The readable `mat.bark` /
`mat.foliage` strings in the examples are stand-ins for those asset ids so the
structure reads clearly; substitute a real material asset id, or omit
`materialRef` to render the primitive with its default look, and skin the part
afterward (see materials-and-looks).

## Worked example one a tree (trunk, branches, leaf clusters)

Three depth levels in one call: a trunk root, two branch children, and leaf
clusters as grandchildren. Local transforms position each part relative to its
parent.

```json
{
  "key": "prefab.tree.oak_lowpoly",
  "name": "Low-poly oak",
  "rootLocalId": "trunk",
  "entities": [
    {
      "localId": "trunk", "key": "tree.trunk", "name": "Trunk",
      "siblingOrder": 0,
      "components": {
        "transform": { "type": "TransformComponent", "position": { "x": 0, "y": 0, "z": 0 }, "scale": { "x": 0.4, "y": 3, "z": 0.4 } },
        "render": { "type": "RenderableComponent", "shape": "capsule", "visible": true, "materialRef": "mat.bark" }
      },
      "identity": {}, "editor": {}, "runtime": {}
    },
    {
      "localId": "branch_l", "key": "tree.branch_l", "name": "Branch left",
      "parentLocalId": "trunk", "siblingOrder": 0,
      "components": {
        "transform": { "type": "TransformComponent", "position": { "x": -0.6, "y": 2.2, "z": 0 }, "rotation": { "x": 0, "y": 0, "z": 40 }, "scale": { "x": 0.18, "y": 1.2, "z": 0.18 } },
        "render": { "type": "RenderableComponent", "shape": "capsule", "visible": true, "materialRef": "mat.bark" }
      },
      "identity": {}, "editor": {}, "runtime": {}
    },
    {
      "localId": "branch_r", "key": "tree.branch_r", "name": "Branch right",
      "parentLocalId": "trunk", "siblingOrder": 1,
      "components": {
        "transform": { "type": "TransformComponent", "position": { "x": 0.6, "y": 2.6, "z": 0.1 }, "rotation": { "x": 0, "y": 0, "z": -35 }, "scale": { "x": 0.16, "y": 1.0, "z": 0.16 } },
        "render": { "type": "RenderableComponent", "shape": "capsule", "visible": true, "materialRef": "mat.bark" }
      },
      "identity": {}, "editor": {}, "runtime": {}
    },
    {
      "localId": "leaves_l", "key": "tree.leaves_l", "name": "Leaf cluster left",
      "parentLocalId": "branch_l", "siblingOrder": 0,
      "components": {
        "transform": { "type": "TransformComponent", "position": { "x": 0, "y": 1.1, "z": 0 }, "scale": { "x": 1.4, "y": 1.2, "z": 1.4 } },
        "render": { "type": "RenderableComponent", "shape": "sphere", "visible": true, "materialRef": "mat.foliage" }
      },
      "identity": {}, "editor": {}, "runtime": {}
    },
    {
      "localId": "leaves_r", "key": "tree.leaves_r", "name": "Leaf cluster right",
      "parentLocalId": "branch_r", "siblingOrder": 0,
      "components": {
        "transform": { "type": "TransformComponent", "position": { "x": 0, "y": 0.9, "z": 0 }, "scale": { "x": 1.2, "y": 1.1, "z": 1.2 } },
        "render": { "type": "RenderableComponent", "shape": "sphere", "visible": true, "materialRef": "mat.foliage" }
      },
      "identity": {}, "editor": {}, "runtime": {}
    },
    {
      "localId": "crown", "key": "tree.crown", "name": "Top crown",
      "parentLocalId": "trunk", "siblingOrder": 2,
      "components": {
        "transform": { "type": "TransformComponent", "position": { "x": 0, "y": 3.2, "z": 0 }, "scale": { "x": 2.2, "y": 2.0, "z": 2.2 } },
        "render": { "type": "RenderableComponent", "shape": "sphere", "visible": true, "materialRef": "mat.foliage" }
      },
      "identity": {}, "editor": {}, "runtime": {}
    }
  ]
}
```

The craft, not the JSON: a believable tree from primitives needs three moves.
Break the silhouette (branches at DIFFERENT angles and lengths, never mirrored),
overlap the foliage volumes so the canopy reads as one mass rather than separate
balls, and let the crown sit slightly off-axis so it does not look stamped. The
subtree makes each of these an independent, tweakable part.

## Worked example two a rock cluster

One base boulder as the root, satellite stones parented to it so moving or
scaling the cluster moves every stone together. Vary each stone's scale and
rotation so no two read alike; that irregularity is what makes rock look like
rock.

```json
{
  "key": "prefab.rock.granite_cluster",
  "name": "Granite cluster",
  "rootLocalId": "boulder",
  "entities": [
    { "localId": "boulder", "key": "rock.boulder", "name": "Boulder",
      "siblingOrder": 0,
      "components": {
        "transform": { "type": "TransformComponent", "position": { "x": 0, "y": 0, "z": 0 }, "rotation": { "x": 10, "y": 30, "z": 5 }, "scale": { "x": 1.8, "y": 1.3, "z": 1.6 } },
        "render": { "type": "RenderableComponent", "shape": "sphere", "visible": true, "materialRef": "mat.granite" } },
      "identity": {}, "editor": {}, "runtime": {} },
    { "localId": "stone_a", "key": "rock.stone_a", "name": "Stone A",
      "parentLocalId": "boulder", "siblingOrder": 0,
      "components": {
        "transform": { "type": "TransformComponent", "position": { "x": 1.1, "y": -0.3, "z": 0.4 }, "rotation": { "x": 25, "y": 70, "z": 15 }, "scale": { "x": 0.7, "y": 0.6, "z": 0.8 } },
        "render": { "type": "RenderableComponent", "shape": "sphere", "visible": true, "materialRef": "mat.granite" } },
      "identity": {}, "editor": {}, "runtime": {} },
    { "localId": "stone_b", "key": "rock.stone_b", "name": "Stone B",
      "parentLocalId": "boulder", "siblingOrder": 1,
      "components": {
        "transform": { "type": "TransformComponent", "position": { "x": -0.9, "y": -0.4, "z": -0.6 }, "rotation": { "x": 5, "y": 200, "z": 40 }, "scale": { "x": 0.5, "y": 0.45, "z": 0.55 } },
        "render": { "type": "RenderableComponent", "shape": "sphere", "visible": true, "materialRef": "mat.granite" } },
      "identity": {}, "editor": {}, "runtime": {} }
  ]
}
```

## Worked example three a bench

A frame root with a seat slab, a backrest, and four legs. Boxes only; the value
is the ARRANGEMENT. Parent every part to the frame so the whole bench places,
rotates, and scales as one object.

```json
{
  "key": "prefab.bench.park",
  "name": "Park bench",
  "rootLocalId": "frame",
  "entities": [
    { "localId": "frame", "key": "bench.frame", "name": "Frame",
      "siblingOrder": 0,
      "components": { "transform": { "type": "TransformComponent", "position": { "x": 0, "y": 0, "z": 0 } } },
      "identity": {}, "editor": {}, "runtime": {} },
    { "localId": "seat", "key": "bench.seat", "name": "Seat",
      "parentLocalId": "frame", "siblingOrder": 0,
      "components": {
        "transform": { "type": "TransformComponent", "position": { "x": 0, "y": 0.45, "z": 0 }, "scale": { "x": 1.8, "y": 0.08, "z": 0.5 } },
        "render": { "type": "RenderableComponent", "shape": "box", "visible": true, "materialRef": "mat.wood" } },
      "identity": {}, "editor": {}, "runtime": {} },
    { "localId": "back", "key": "bench.back", "name": "Backrest",
      "parentLocalId": "frame", "siblingOrder": 1,
      "components": {
        "transform": { "type": "TransformComponent", "position": { "x": 0, "y": 0.85, "z": -0.22 }, "scale": { "x": 1.8, "y": 0.5, "z": 0.06 } },
        "render": { "type": "RenderableComponent", "shape": "box", "visible": true, "materialRef": "mat.wood" } },
      "identity": {}, "editor": {}, "runtime": {} },
    { "localId": "leg_fl", "key": "bench.leg_fl", "name": "Leg front-left",
      "parentLocalId": "frame", "siblingOrder": 2,
      "components": {
        "transform": { "type": "TransformComponent", "position": { "x": -0.8, "y": 0.22, "z": 0.2 }, "scale": { "x": 0.08, "y": 0.45, "z": 0.08 } },
        "render": { "type": "RenderableComponent", "shape": "box", "visible": true, "materialRef": "mat.metal" } },
      "identity": {}, "editor": {}, "runtime": {} }
  ]
}
```

Add the remaining three legs the same way (`leg_fr`, `leg_bl`, `leg_br`) with
mirrored x/z. A collider on `frame` (a single box `ColliderComponent` sized to
the seat) is usually enough; you rarely need a collider per plank.

## Nesting a prefab as a part

A part can BE an instance of another prefab. Give the entity a `prefabInstance`
instead of authoring its geometry inline:

```json
{
  "localId": "canopy", "key": "tree.canopy", "name": "Canopy",
  "parentLocalId": "trunk", "siblingOrder": 3,
  "prefabInstance": {
    "prefabKey": "prefab.foliage.leaf_cluster",
    "overrides": [
      { "entityLocalId": "cluster_root", "componentKey": "render", "propertyPath": "materialRef", "op": "set", "value": "mat.autumn_leaves" }
    ]
  },
  "identity": {}, "editor": {}, "runtime": {}
}
```

Now the leaf cluster is authored ONCE as its own prefab and reused across every
tree: the part carries a reference to the other prefab (by `prefabId` or
`prefabKey`, with its own per-instance `overrides`), and the referenced prefab is
resolved and its overrides validated when the parent prefab is materialized (the
create path checks the nested reference and folds its overrides). Prefer this
when a sub-assembly repeats: author the leaf cluster, the wheel, or the lamp head
once, then reference it from every prop that uses it, rather than pasting its
geometry into each parent.

## Variants re-skinning one base prefab

To ship a family (oak / pine / dead tree) without re-authoring the trunk-and-
branch structure, set `basePrefabKey` and a `variantOverrides` array. When the
prefab resolves, its base lineage is walked and each variant's override layer is
folded down onto the base entities.

Know exactly which override ops the shared effective-entity resolver ACTUALLY
applies, so you author overrides that take effect rather than silently no-op. The
resolver (`applyPrefabOverrideToEntity` in runtime-shared/prefab.ts) implements:

- `set` (a component field at `propertyPath`, needs `componentKey`),
- `component_add` / `component_remove` (needs `componentKey`),
- `name_set`, `scripts_set`, `tag_add` / `tag_remove`, `runtime_set`,
- `hierarchy_reorder_child` (re-parent or reorder an EXISTING part: set its
  `parentLocalId` or its `siblingOrder`).

Example: recolor the crown and re-parent a branch in a variant.

```json
{
  "key": "prefab.tree.oak_autumn",
  "name": "Autumn oak",
  "basePrefabKey": "prefab.tree.oak_lowpoly",
  "rootLocalId": "trunk",
  "entities": [ /* the base body, cloned */ ],
  "variantOverrides": [
    { "entityLocalId": "crown", "componentKey": "render", "propertyPath": "materialRef", "op": "set", "value": "mat.autumn_foliage" },
    { "entityLocalId": "leaves_r", "propertyPath": "siblingOrder", "op": "hierarchy_reorder_child", "value": 2 }
  ]
}
```

ADDING a part in a variant is NOT done with an override. The
`hierarchy_add_child` and `hierarchy_remove_child` ops exist in the
`PrefabOverride` op enum and pass validation, but the shared effective-entity
resolver does NOT apply them (they fall through as no-ops); treat them as a
recorded gap, not a working add-a-child mechanism. To add a NEW part that the
base lacks, include it as a fresh entity in the VARIANT'S OWN `entities` array
with a `parentLocalId` into the base structure. Any variant entity whose
`localId` is not in the base is appended to the resolved set:

```json
{
  "key": "prefab.tree.oak_winter",
  "name": "Winter oak",
  "basePrefabKey": "prefab.tree.oak_lowpoly",
  "rootLocalId": "trunk",
  "entities": [
    { "localId": "snow_cap", "key": "tree.snow_cap", "name": "Snow cap",
      "parentLocalId": "crown", "siblingOrder": 0,
      "components": {
        "transform": { "type": "TransformComponent", "position": { "x": 0, "y": 1.0, "z": 0 }, "scale": { "x": 2.3, "y": 0.4, "z": 2.3 } },
        "render": { "type": "RenderableComponent", "shape": "sphere", "visible": true, "materialRef": "mat.snow" } },
      "identity": {}, "editor": {}, "runtime": {} }
  ],
  "variantOverrides": [
    { "entityLocalId": "crown", "componentKey": "render", "propertyPath": "materialRef", "op": "set", "value": "mat.snow_foliage" }
  ]
}
```

The variant keeps the shared structure and layers only the differences (a re-skin
override plus the one new snow-cap entity), so fixing the base trunk fixes every
variant.

## Instance the finished prop, do not re-author it

Once the prop is a prefab, place copies with `project_create_entity`
(`project.entity.create`) passing `prefabId` (a uuid or the prefab key). Each
call drops one prefab instance; `componentOverrides` on the call re-pose or
re-scale that instance (a shorter tree, a rotated bench) without touching the
prefab. This is the correct way to fill a scene with the same prop:

- Author the tree prefab ONCE.
- Place fifteen trees with fifteen `project_create_entity` calls, each with a
  different `TransformComponent` position and a small scale/rotation override so
  the copies do not look stamped.

Do NOT paste the whole `entities` subtree fifteen times. Re-authoring the body
per copy bloats the graph, loses the single-edit point, and is the exact waste
the prefab exists to prevent. For the runtime detail of how a prefab instance
resolves (it places the prefab root as one instance entity, not an expanded
graph subtree) and for placing HUNDREDS of copies of one SIMPLE mesh via an
instanceSet instead of per-entity, see asset-to-instances (steps 3a and 3b).

## Compose from primitives, or request generation

Composing a prop from a primitive subtree and minting a generated mesh are
different tools for different shapes. Choose by where the fidelity lives:

- COMPOSE from primitives when the object reads from its GEOMETRY and
  arrangement: benches, crates, fences, lamp posts, simple carts, blocky or
  low-poly trees and rocks, anything you would model by stacking boxes, spheres
  and capsules. It is instant, costs no credits, stays fully editable part by part,
  and every piece is a real authored entity you can move or script.
- WELD with CSG (see model-authoring-parametric-csg) when the parts must fuse
  into ONE continuous surface, for example a keyhole cut through a plate or a
  socket bored into a block. CSG produces a single mesh; a prefab subtree keeps
  the parts distinct. Use a prefab when the parts should read or move
  separately; use CSG when they are one solid.
- REQUEST GENERATION (see asset-acquisition-strategy) when the value is SURFACE
  detail primitives cannot express: bark and leaf cards on a hero tree, a
  detailed creature, cloth folds, ornate carving. Spend the credits on the few
  hero objects the player studies up close, and compose primitives for the
  background fill.

A good instinct: reach for composition first for the many mid- and background
props (it is free and immediate), and escalate to generation only for the hero
objects where a primitive silhouette would break the illusion.

## Pitfalls

- `rootLocalId` MUST name a real `localId` in `entities`, and every
  `parentLocalId` must reference a `localId` that exists in the same array. A
  dangling parent id is a broken subtree.
- Child transforms are LOCAL to the parent. If a branch flies off into space, you
  posed it in world coordinates by habit; halve the numbers and think relative to
  the trunk.
- Do not give every plank its own collider. One box `ColliderComponent` sized to
  the prop is cheaper and usually indistinguishable in play.
- A variant that re-authors the whole body instead of using `variantOverrides`
  loses the shared-edit point; keep the base body canonical and layer only diffs.
- Instancing a prefab is one instance entity per call; there is no one-call bulk
  fan-out of a multi-entity prefab on the authoring surface (asset-to-instances
  names this explicitly). Issue N `project_create_entity` calls, or use an
  instanceSet for repeated simple GEOMETRY.
