# Composites

> Reference for the Decentraland `.composite` JSON format that declares the initial entities of a scene in `assets/scene/main.composite`. Use when authoring a main.composite from scratch, when editing one by hand because the Creator Hub MCP is not available (see creator-hub-mcp — the MCP is always preferred for an existing scene), or when other skills point to the composite reference. For scaffolding a whole scene project see create-scene.

- Skill: `decentraland/composites` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add decentraland/composites`
- Raw SKILL.md: https://api.skillmd.com/api/skills/decentraland/composites/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: decentraland (https://skillmd.com/u/decentraland)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/decentraland/composites

---


# Composites

This skill carries the shared composite format reference used by other Decentraland skills (`create-scene`, `add-3d-models`, `sdk-scenes`).

The mandatory workflow below applies to EVERY composite you author or edit: check the Creator Hub MCP gate first, compute scene bounds (Step 0), consult the format catalog in the reference while writing entities, then run the validation gate at the end before finishing.

## Gate — use the Creator Hub MCP instead of editing the file (check FIRST)

**Hand-editing `main.composite` is the fallback, not the default.** The Creator Hub exposes an MCP server (skill: **creator-hub-mcp**) whose tools — `scene_state`, `create_entity`, `set_component`, `remove_entity`, `place_smart_item`, `attach_script`, `set_scene_settings`, … — change the open scene live: the viewport updates, the editor autosaves the composite itself, `inspector::Nodes` and the rest of the editor bookkeeping stay consistent, and every change is undoable by the user. Editing the file instead risks the classic failure: the editor's autosave regenerates the file wholesale and your work silently vanishes.

Before touching the file, pick the first row that matches:

1. **Creator Hub MCP tools are available in the session** (`mcp__creator-hub__*` in Claude Code; bare `scene_state` / `create_entity` inside the Creator Hub's own AI assistant) → do NOT edit `main.composite`, `main.crdt`, or `scene.json` by hand. Use the tools for every entity, component, and settings change. The format catalog in this skill remains your reference for component shapes — `set_component` takes the same JSON the composite stores — and Step 0 (bounds) still applies.
2. **The Creator Hub is running but the tools are not connected** → tell the user the MCP is the safer route and how to connect (`{baseDir}/../creator-hub-mcp/reference/connect.md`: Settings > Experimental > *Expose AI assistant MCP server*, copy the snippet). Fall back to file editing only if they decline, and only with the scene closed in the Creator Hub.
3. **No Creator Hub in play** (CLI-only project, CI, the app is closed and the user wants it that way) → the file workflow below applies in full.

## Step 0 — Read scene.json and Compute Bounds (MANDATORY)

**Before writing a single entity position, read `scene.json` and calculate the scene bounds.** This must happen first — all entity positions must fit within these bounds or they will not render.

### How to calculate bounds

1. Open `scene.json` and locate `scene.parcels` (array of `"x,y"` strings) and `scene.base`.
2. Parse every parcel as integers. Find the min and max X and Y across all parcels.
3. Compute:

```
parcelsWide = max(parcel_x) - min(parcel_x) + 1
parcelsDeep = max(parcel_y) - min(parcel_y) + 1

maxX = parcelsWide * 16
maxZ = parcelsDeep * 16
```

4. Valid entity positions: **X in [0, maxX], Z in [0, maxZ]**. Negative values and values above maxX/maxZ are outside the scene and will not render.

### Step 0b — Account for 3D Model Bounding Boxes (MANDATORY for GLB models)

**A model's `Transform.position` is its local origin, NOT its visual extent.** Tree and vegetation models commonly extend 6–12 m _beyond_ their origin in one or more directions. Placing a tree at x=2 can cause it to render at x=–10, which is outside the scene bounds.

**How to find a model's bounding box** — parse the GLB binary and apply node-level transforms. Raw accessor `min`/`max` values alone are **not reliable** because many GLB models have large scale factors or translations baked into the GLTF node hierarchy (e.g. a model whose accessors say 0.6 m but whose node scale is 24× giving an actual rendered size of 14 m).

```js
node -e "
const buf = require('fs').readFileSync('assets/Models/MyModel.glb');
const jsonLen = buf.readUInt32LE(12);
const json = JSON.parse(buf.slice(20, 20+jsonLen));
let minW=[Infinity,Infinity,Infinity], maxW=[-Infinity,-Infinity,-Infinity];
json.nodes?.forEach(n => {
  if (n.mesh === undefined) return;
  const s = n.scale || [1,1,1];
  const t = n.translation || [0,0,0];
  for (const prim of json.meshes[n.mesh].primitives) {
    const acc = json.accessors[prim.attributes.POSITION];
    if (!acc.min || !acc.max) continue;
    for (let i = 0; i < 3; i++) {
      const lo = acc.min[i]*s[i]+t[i], hi = acc.max[i]*s[i]+t[i];
      minW[i] = Math.min(minW[i], lo, hi);
      maxW[i] = Math.max(maxW[i], lo, hi);
    }
  }
});
const w=maxW[0]-minW[0], h=maxW[1]-minW[1], d=maxW[2]-minW[2];
console.log('Rendered size:', w.toFixed(2)+'m x', h.toFixed(2)+'m x', d.toFixed(2)+'m');
console.log('World min:', minW.map(v=>v.toFixed(2)), 'max:', maxW.map(v=>v.toFixed(2)));
"
```

**Measure per model — don't guess or hard-code.** Extents vary wildly: running the script above on a typical tree often reveals ~11 m of reach in one horizontal direction from the origin (safe minimum origin z≥12), while a column reaches under 1 m in every direction. Always compute the box for the specific GLB you're placing.

**Rule:** For every GLB model, compute:

```
minSafeX = max(0, -bbox.minX) + margin      (≥1 m)
minSafeZ = max(0, -bbox.minZ) + margin      (≥1 m)
maxSafeX = maxX - (bbox.maxX + margin)
maxSafeZ = maxZ - (bbox.maxZ + margin)
```

Only place the model if its Transform position satisfies all four bounds.

For tree/vegetation models where the bounding box is unknown, assume a **12 m safe buffer** from all edges — i.e., place origins in `[12, maxX-12]` × `[12, maxZ-12]`.

### Examples

| scene.json parcels          | parcelsWide | parcelsDeep | Valid X | Valid Z |
| --------------------------- | ----------- | ----------- | ------- | ------- |
| `["0,0"]`                   | 1           | 1           | 0 – 16  | 0 – 16  |
| `["0,0","1,0"]`             | 2           | 1           | 0 – 32  | 0 – 16  |
| `["0,0","1,0","0,1","1,1"]` | 2           | 2           | 0 – 32  | 0 – 32  |

### Never change scene.json parcel count without explicit user instruction

Adding parcels to `scene.json` is not always an option, it depends where the scene will be published to. If publishing to Genesis City, parcels must be **owned or rented** by the deploying wallet; if publishing to a World, it might be an option. If the scene is currently too small for what the user is asking for, ask the user for confirmation to change the scene layout and include more parcels. If they disagree then **work within the existing parcel bounds and make the scene as rich as possible within 16×16m**. Do not silently expand the parcel list. If more space is truly needed, ask the user first.

## Composite format catalog (reference)

Read `{baseDir}/composite-reference.md` for the full specification of the `main.composite` JSON format: the JSON structure and `jsonSchema` rules, entity ID allocation, the per-component format catalog (`core::Transform`, `core::GltfContainer`, … `core::NftShape`), the component-grouping pattern, edit-mode rules (`inspector::Nodes`), runtime spawning, and patterns for fetching composite entities from TypeScript.

## Validation Checklist

**Step 1 — Detect mode.** Scan the composite for `inspector::*`, `composite::root`, or `asset-packs::ActionTypes`. If any are present, you are in **edit mode** — use the edit-mode checklist below. Otherwise use the authoring-from-scratch checklist.

### Authoring-from-scratch checklist

Before writing a fresh composite, verify:

- [ ] `version` is `1`
- [ ] NO `inspector::*` components whatsoever — no `inspector::Nodes`, `inspector::SceneMetadata` (any version), `inspector::Selection`, `inspector::TransformConfig`, `inspector::UIState`. These are all auto-generated by the Creator Hub and including them in a fresh file breaks the entity tree or causes build errors.
- [ ] NO `composite::root` or `asset-packs::ActionTypes` — auto-generated by engine
- [ ] Every user entity (512+) has `core::Transform` and `core-schema::Name`
- [ ] No duplicate entity IDs across the composite
- [ ] No duplicate entity IDs with entities created via code with an explicit ID
- [ ] `core::` components do NOT have `jsonSchema` — this is a hard requirement; including jsonSchema on a core:: component will cause the Creator Hub to fail to parse entities correctly
- [ ] Non-core components (`asset-packs::*`, `core-schema::*`) MUST have `jsonSchema` (copied from catalog)
- [ ] All `GltfContainer.src` paths use slugified name format: `assets/asset-packs/<slug>/<filename>`
- [ ] All referenced asset files were downloaded to disk (GLB, audio, images)
- [ ] Default collision masks set on GltfContainer (`visibleMeshesCollisionMask: 0`, `invisibleMeshesCollisionMask: 3`)
- [ ] All positions within parcel bounds — bounds were calculated in **Step 0** from the actual `scene.json` parcel list. Every entity's X is in `[0, maxX]` and Z is in `[0, maxZ]`. Negative values and values above maxX/maxZ do not render. If the user requested a "large" scene but parcel count was not changed, all entities fit within the original bounds.
- [ ] For every `GltfContainer` entity: checked whether the GLB contains animations (clip names embedded in the file). If it does, an `core::Animator` component is present on that entity. A model with animations but no Animator will silently loop its first clip with no way to control it.
- [ ] For every `GltfContainer` entity: checked whether the GLB contains collision meshes (any mesh whose name includes the string `_collider`). If yes, `invisibleMeshesCollisionMask` is set to `3` (CL_POINTER + CL_PHYSICS) to activate them. If no built-in colliders, evaluated whether a `core::MeshCollider` box/sphere is needed to cover the model's rough shape (for walkable surfaces, walls, or clickable objects).
- [ ] If `asset-packs::Actions`, `asset-packs::Triggers`, or `asset-packs::States` exist anywhere in the composite, then `asset-packs::Counter` must exist on entity 0, with `value` = the highest `id` used inside any Actions/Triggers/States data (it is the id allocator for those ids)
- [ ] No `{self}`, `{assetPath}`, or placeholder strings — all resolved to concrete values
- [ ] Component names use base names (e.g., `asset-packs::Actions`, not `asset-packs::Actions-v1`). Never use versioned suffixes like `-v3`.
- [ ] A composite using only core components needs no extra library. If it contains `asset-packs::*` components, older SDKs require `@dcl/asset-packs` as a project dependency; current SDKs fall back to the copy bundled inside `@dcl/inspector`

### Edit-mode checklist (composite already contains `inspector::*`)

- [ ] **The Creator Hub MCP is NOT available.** If its tools are in the session, stop — every item below is done for you by `create_entity` / `set_component` (see the gate at the top of this skill and **creator-hub-mcp**).
- [ ] **The scene is NOT currently open in the Creator Hub.** The inspector autosaves and overwrites `main.composite` wholesale from its in-memory engine, discarding external edits with no error. Ask the user to close the scene before you write, and to reopen it afterwards.

For every NEW entity `<id>` you add, in addition to the authoring-from-scratch rules above (with the relaxation that `inspector::*` etc. are kept, not stripped):

- [ ] `<id>` has been appended to the **end** of the `children` array of the entity-`0` entry inside `inspector::Nodes.data["0"].json.value`.
- [ ] A new entry `{ "entity": <id>, "children": [...] }` has been appended to the `value` array of `inspector::Nodes.data["0"].json` (use `[]` if the entity has no children of its own).
- [ ] If `<id>`'s `Transform.parent` is not `0`, then `<id>` is in the parent entity's `children` array (not the root's).
- [ ] `core-schema::Name.data["<id>"]` has a `{ "json": { "value": "..." } }` entry — names are required for the entity to appear correctly in the entity tree and to be looked up by code.
- [ ] `inspector::TransformConfig.data["<id>"]` has a `{ "json": {} }` entry (empty object is fine).
- [ ] `entity-names.ts` is either updated to include the new name (in `EntityNames`) OR left untouched so the Creator Hub regenerates it on next save. Do NOT hand-edit the auto-generated header.
- [ ] You did NOT delete or strip pre-existing `inspector::Nodes`, `inspector::SceneMetadata-*`, `inspector::Selection`, `inspector::TransformConfig`, `composite::root`, or `asset-packs::ActionTypes`. These are managed by the Creator Hub and must stay.
- [ ] `inspector::SceneMetadata-*` is unchanged unless `scene.json` parcels changed (in which case the layout block must match `scene.json`).
- [ ] Reserved entities `1` (PlayerEntity) and `2` (CameraEntity) are still present in `inspector::Nodes` as their own top-level `{ "entity": 1, "children": [] }` / `{ "entity": 2, "children": [] }` entries. They are **separate tree roots** — they must NOT appear inside entity `0`'s `children` array (or any other `children` array).

**Verification command (edit mode):** after editing, every entity ID present in `core::Transform.data` should also appear:

1. As a top-level `{ "entity": <id>, ... }` entry in `inspector::Nodes.data["0"].json.value`, AND
2. In exactly one `children` array within that same `value` list (its parent's children).

Entities `0`, `1`, and `2` are the exception to rule 2 — they are tree roots and appear in no `children` array.

Missing entries here are the root cause of "entity renders but is invisible in the Creator Hub entity tree".

## Post-Write Validation

After writing the composite, **run the SDK build** to verify:

```bash
npx sdk-commands build
```

The build must pass with zero errors. If it fails, the composite is invalid. Common errors:

- `Composite references undefined component "X". Ensure provider.schemas was registered pre-seal via setCompositeProvider().` (older/released SDKs word this as `"X is not defined and there is no schema to define it"`) → missing `jsonSchema` on non-core component, or `inspector::*` component that shouldn't be there
- TypeScript errors → fix generated scripts

