Customization
Each manifest's customization.capabilities is the complete list of what may change on an instance — in the browser, through the SDK, without Blender. Anything else is refused with a structured error.
Every asset and every refusal, with the alternative, is listed on capabilities and limits.
Capability levels
| Level | What | Blender |
|---|---|---|
| L1 | Load and place the published GLB as-is (22 assets) | not needed |
| L2 | Declared runtime capabilities: recolour, swatches, uniform scale, visibility toggles, part swaps (8 assets) | not needed |
| L3 | Scene assembly with connector snapping (scene 1.1.0), clips | not needed |
| L4 | Geometry authoring (new shapes, storeys, roof pitch) | outside this kit (no .blend sources included) |
Assets with capabilities
| Asset | Capabilities |
|---|---|
| stylized-town/bench-01@0.4.0 | wood-color (material-color), size-scale (uniform-scale), armrests (node-visibility) |
| stylized-town/cafe-01@0.1.0 | front-door (modular-swap), awning (node-visibility), terrace (node-visibility), wall-color (material-color) |
| stylized-town/house-01@0.2.0 | front-door (modular-swap), window-style (modular-swap), roof (modular-swap), chimney (node-visibility), wall-color (material-color), roof-color (material-color) |
| stylized-town/house-02@0.1.0 | front-door (modular-swap), window-style (modular-swap), roof (modular-swap), wall-color (material-color), roof-color (material-color) |
| stylized-town/market-stall-01@0.1.0 | canopy-color (material-color) |
| stylized-town/shop-01@0.2.0 | front-door (modular-swap), awning (node-visibility), wall-color (material-color) |
| stylized-town/street-lamp-01@0.3.0 | light (node-visibility) |
| stylized-town/townhall-01@0.1.0 | flag (node-visibility), wall-color (material-color) |
Inspect, write a config, validate
Run these in a project with the kit installed (after the quickstart). The example uses stylized-town/house-01@0.2.0, the asset with the most capabilities.
npx agent-ready-assets inspect stylized-town/house-01@0.2.0 --capabilities --connectors{
"schema_version": "1.0.0",
"asset": "stylized-town/house-01@0.2.0",
"values": {
"front-door": "door-02",
"window-style": "window-02",
"roof": "roof-hip-01",
"chimney": false,
"wall-color": "paving",
"roof-color": "curb-stone"
}
}Configs are absolute (every capability not listed uses its default) and all-or-nothing: one invalid value rejects the whole config.
npx agent-ready-assets customize stylized-town/house-01@0.2.0 --config my-config.json --output src/generated/house-01.customization.jsonThe output file is the validated config in canonical form, not a modified GLB: nothing imports it for you. Import it in your code and pass its values to library.instantiate (below), or write the same values into a scene's customize block. verify-project checks every config file it finds in the project.
Check a config without writing anything
--dry-run (SDK 0.5.1) validates the config exactly like a real run and writes nothing, so --output is optional and may point anywhere. A target a real run would refuse — outside the project, or an existing file without --force — is named in warnings instead:
npx agent-ready-assets customize stylized-town/house-01@0.2.0 --config my-config.json --output /tmp/house-01.customization.json --dry-runWithout --dry-run the same command is refused (exit 1, PATH_OUTSIDE_PROJECT, nothing written): real runs write only inside the project.
Use it in code
const library = await AssetLibrary.fromKit('/assets/stylized-town/kit.json');
const config = {"schema_version":"1.0.0","asset":"stylized-town/house-01@0.2.0","values":{"front-door":"door-02","window-style":"window-02","roof":"roof-hip-01","chimney":false,"wall-color":"paving","roof-color":"curb-stone"}};
const instance = await library.instantiate(config.asset, config.values); // all-or-nothing: throws CustomizationError on invalid input
scene.add(instance.object);
instance.parts; // parts mounted by modular-swap: [{ capability, choice, slot, ref, plug, asset }] (default choices stay baked)
instance.hiddenNodes; // optional parts hidden on this instance only
// the mounted stylized-town/door-02@0.1.0 brings the clip "open": play it on that part instance (import { playClip } from '@agent-ready-assets/runtime-three')
const part = instance.parts.find((p) => p.slot === 'anchor-door-main')?.asset;
const player = part ? playClip(part, "open", { loop: 'pingpong' }) : null; // per frame: player?.update(deltaSeconds)
// later: instance.release(); // disposes its own materials and releases the mounted parts// customize accepts only declared capabilities (swap choice ids, booleans for toggles, swatch names for palette colours);
// anything else calls onError with a structured error. Swaps load their part asynchronously; unmounting releases it.
<Asset assetId="stylized-town/house-01@0.2.0" customize={{"front-door":"door-02","window-style":"window-02","roof":"roof-hip-01","chimney":false,"wall-color":"paving","roof-color":"curb-stone"}} clip={{ name: "open", slot: 'anchor-door-main', loop: 'pingpong' }} onError={(e) => console.error(e)} />Re-customize an instance of an assembled scene
await town.customize(instanceId, values) (SDK 0.5.1) replaces one instance of an assembleScene result with a new instance of the same asset. Values are absolute, like a config. The whole scene is validated again first (values, swap parts, snaps, overlaps); on any error nothing changes. The optimization, if any, is rebuilt. In React Three Fiber, change that <Asset>'s customize prop instead.
const town = await library.assembleScene(sceneJson, { optimize: true });
// "house-main-b" is a stylized-town/house-01@0.2.0 instance of town-customized
await town.customize('house-main-b', {"front-door":"door-02","window-style":"window-02","roof":"roof-hip-01","chimney":false,"wall-color":"paving","roof-color":"curb-stone"});
// CustomizationError on any invalid value (nothing changes); RELEASED_SOURCE after town.release(); INSTANCE_UNKNOWN for an id not in the sceneVariants of one base asset
Buildings that look different but share one model are variants of one base asset: place the same asset several times and give each instance its own declared choices — part swaps (front-door, window-style, roof) and palette swatches (wall-color). Two different assets whose defaults happen to differ are not variants: they are two models.
- Another footprint or shape → another asset. The kit's other buildings: stylized-town/cafe-01@0.1.0 (4 × 8 m lot), stylized-town/house-02@0.1.0 (4 × 7.2 m lot), stylized-town/shop-01@0.2.0 (4 × 6.4 m lot), stylized-town/townhall-01@0.1.0 (8 × 8 m lot).
stylized-town/house-02@0.1.0declares the same part swaps but is another model — 4 × 7.2 m lot and 6.02 m high, wherestylized-town/house-01@0.2.0has a 4 × 6.2 m lot and is 8.61 m high: one of each is two models, not two variants of house-01. - Variants of one model → its part swaps and swatches. Every value must be one of the asset's declared choices; check them with
npx agent-ready-assets inspect stylized-town/house-01@0.2.0 --capabilities(or the MCP toolget_customization_capabilities). - A default choice is baked into the GLB (nothing is mounted); any other choice mounts its pinned part on that instance only. Each instance owns its parts and colours; the other instances and the cached GLB never change.
| Capability | house-01-a | house-01-b | Declared choices |
|---|---|---|---|
front-door — Part swap | door-02 | door-01 (default) | door-01 → stylized-town/door-01@0.1.0, door-02 → stylized-town/door-02@0.1.0 |
window-style — Part swap | window-01 (default) | window-02 | window-01 → stylized-town/window-01@0.1.0, window-02 → stylized-town/window-02@0.1.0 |
roof — Part swap | roof-hip-01 | roof-gable-01 (default) | roof-gable-01 → stylized-town/roof-gable-01@0.1.0, roof-hip-01 → stylized-town/roof-hip-01@0.1.0 |
wall-color — Recolour | paving | leaf | ivory, paving, leaf |
Both variants differ from the asset's defaults — house-01-a in front-door, roof, wall-color, house-01-b in window-style, wall-color — and from each other in every varied capability, so each renders differently from the plain asset and from the other. An instance at the defaults is the plain asset, not a variant.
Left at their defaults in this example: chimney, roof-color.
In one scene: the two variants of stylized-town/house-01@0.2.0 snapped by their lot fronts (attach_to) to two joined road tiles, stylized-town/road-straight-01@0.3.0 (Straight road tile (4 m)) — 4 instances of 2 assets, 3 snapped. Save it as src/scenes/variants.scene.json in a project with the kit installed and validate it:
{
"schema_version": "1.1.0",
"kit": "stylized-town@0.4.0",
"instances": [
{
"instance_id": "road-straight-1",
"asset": "stylized-town/road-straight-01@0.3.0",
"position_m": [
0,
0,
0
],
"rotation_euler_rad": [
0,
0,
0
],
"uniform_scale": 1,
"customize": {}
},
{
"instance_id": "road-straight-2",
"asset": "stylized-town/road-straight-01@0.3.0",
"uniform_scale": 1,
"customize": {},
"attach_to": {
"instance_id": "road-straight-1",
"anchor_id": "anchor-road-east",
"connector": "anchor-road-west"
}
},
{
"instance_id": "house-01-a",
"asset": "stylized-town/house-01@0.2.0",
"uniform_scale": 1,
"customize": {
"front-door": "door-02",
"window-style": "window-01",
"roof": "roof-hip-01",
"wall-color": "paving"
},
"attach_to": {
"instance_id": "road-straight-1",
"anchor_id": "anchor-side-south",
"connector": "anchor-lot-front"
}
},
{
"instance_id": "house-01-b",
"asset": "stylized-town/house-01@0.2.0",
"uniform_scale": 1,
"customize": {
"front-door": "door-01",
"window-style": "window-02",
"roof": "roof-gable-01",
"wall-color": "leaf"
},
"attach_to": {
"instance_id": "road-straight-2",
"anchor_id": "anchor-side-south",
"connector": "anchor-lot-front"
}
}
]
}npx agent-ready-assets scene validate src/scenes/variants.scene.json --resolvePrints every resolved transform with the measured mate. Then npx agent-ready-assets verify-project --browser chromium renders every scene file of the project, this one included (LOCALLY_VALIDATED).
import variants from './scenes/variants.scene.json'; // a Vite JSON import
const town = await library.assembleScene(variants, { optimize: true }); // validated and snapped, all-or-nothing
scene.add(town.group);Unsupported requests are refused
An agent asked for something the manifest does not declare gets a structured error with the allowed alternatives — never an approximation. Every refusal with its documented alternative: capabilities and limits.
{
"schema_version": "1.0.0",
"asset": "stylized-town/house-01@0.2.0",
"values": {
"storeys": 3
}
}npx agent-ready-assets customize stylized-town/house-01@0.2.0 --config storeys.json --output src/generated/storeys.json| Request | Result |
|---|---|
{ "kind": ... } | UNSUPPORTED_OPERATION: producer recipe parameter, not a Blender-free capability (needs L4 authoring) |
{ "width-m": ... } | UNSUPPORTED_OPERATION: producer recipe parameter, not a Blender-free capability (needs L4 authoring) |
{ "depth-m": ... } | UNSUPPORTED_OPERATION: producer recipe parameter, not a Blender-free capability (needs L4 authoring) |
{ "body-z-m": ... } | UNSUPPORTED_OPERATION: producer recipe parameter, not a Blender-free capability (needs L4 authoring) |
{ "plinth-height-m": ... } | UNSUPPORTED_OPERATION: producer recipe parameter, not a Blender-free capability (needs L4 authoring) |
{ "storey-height-m": ... } | UNSUPPORTED_OPERATION: producer recipe parameter, not a Blender-free capability (needs L4 authoring) |
recolor or edit a baked texture | UNSUPPORTED_OPERATION: only declared material colours can change |
add, remove or reshape geometry (e.g. extra floors or storeys, a taller tower, a roof pitch) | UNSUPPORTED_OPERATION: only the declared parts can be swapped or hidden; no geometry operation is offered |
a part that is not in the swap allowlist (or the part ref instead of the choice id) | INCOMPATIBLE_PART/PART_NOT_ALLOWED: only the declared choices exist; the hint lists them |
a free #RRGGBB colour on a palette-swatch capability | INVALID_PARAMETER/NOT_IN_CHOICES: use one of the swatch names |
a visibility toggle given as a string ("false") | INVALID_PARAMETER/WRONG_TYPE: toggles are booleans |
hiding a part that is not declared optional (walls, roof, a slot part) | UNSUPPORTED_OPERATION/UNKNOWN_CAPABILITY: only declared node-visibility capabilities exist |
a swap part that is not installed in your kit | INCOMPATIBLE_PART/PART_NOT_IN_KIT: install the complete kit (or `add` the host, which installs its pinned parts) |
snapping a part instance into a host slot in scene.json | VALIDATION_FAILED/SNAP_OCCUPIED: slots change only through the modular-swap capability |
Evidence labels
- A tested preset (its
config_sha256is in the asset's quality report) was rendered and measured before publication. - Any other legal configuration is
SUPPORTED_BUT_UNTESTEDuntil yourverify-project --browser chromiumrun marks itLOCALLY_VALIDATED. PRESET_VERIFIEDneeds a released package with a recorded approval of that preset by a person; the asset's quality report shows whether it has one.