Tools & quick phrases
What the MCP server lets Claude do (list, read, create, edit, and publish your scenes), and the phrases that trigger each tool.
Open in the interactive manual →The interactive version has full-text search, a table of contents, and light/dark themes.
Once connected, Claude gains a small set of tools for working with your scenes:
The connector also offers four ready-made prompts, which apps such as Claude list in their connector menu: Make my first 3D scene (an open-air gallery of the pictures on your site, or your own idea in a sentence), A park walk past my pictures, Walk through my site, and I would rather use the web editor (just the editor link). Pick one and Claude builds it in that chat. A gallery built this way is interactive from the first build: every framed print carries an Inspect tap action titled with the picture's name, so a visitor who taps it gets the print in the fullscreen viewer, and each stand or sign post carries Travel to it, so tapping a post flies the visitor over. The pictures are the subject: 1.6 to 1.8 m frames at eye height, with the arrival camera 7 to 9 m away and at most five prints in the opening shot; the sign back to your site is the way out, never the main action.
| Tool | Does |
|---|---|
list_scenes | List your saved scenes, 40 at a time (q searches by name or slug; page with nextOffset). |
get_scene | Read a compact summary of one scene (nodes, transforms, materials). On a big scene it narrows and pages rather than failing: see below. |
list_assets | Search the images, models and sounds in your account and the shared public library (picture frames, props, texture sets) by label and keyword, and hand back the address of one. This is how Claude textures something with a picture you already own instead of generating (and charging for) a new one. A shared result is copied into your library before it is placed, so the page never depends on someone else keeping theirs. |
upload_asset | Put a file into your asset library and hand back its address: a model, an image, a sound, a video, an HDR or a LUT (a .cube or .3dl file lands as a colour-grading LUT). It comes from a public link (a picture up to 12 MB, a .glb model up to 36 MB, or a sound), from the file itself (up to about 3 MB on the hosted connector, about 35 MB on a local server), or, when the MCP server runs on your own computer, from a file on disk. A model gets its phone-sized copy made automatically, the same as an editor upload, and an image answers its width, height and aspect in pixels, so a picture plane can be cut to the real shape instead of a guess. A label and keywords make it findable again with list_assets. |
compose_surface | Build a surface that reads as printed on a real material: a quote on a stretched canvas, a poster, a shop sign, a plaque. See Printed surfaces below. |
measure | Measure objects in a scene without rendering it: each one's world centre, size and box, the clearance to the nearest thing in every direction, the distance and gap between pairs, and for an imported model the opening through it (a picture frame, a doorway, a window). get_scene only ever knew a model's outside. |
create_scene | Make a new scene: empty, or a copy of one of yours (copyFrom). |
apply_ops | Apply a batch of edits: the same add/move/recolor/group operations as the in-editor Assistant, including placing point & spot lights (with a gobo pattern such as dapple or blinds, its drift, and a volumetric beam, so "sun through leaves on the floor" is one light rather than a trick), hanging cables (sagging power lines, ropes, festoon strings) between two points, laying a shadow pool under a heavy object so it reads as resting on the ground, steering particles with a force field, the scatter op for set-dressing, the settings op for the scene itself (see below), and the make_page and make_diagram composites below. It can also write keyframe animation onto an object: arbitrary position, rotation, and scale keys, well past what the editor's preset menu offers. Each call is atomic and checkpointed: the owner can roll back in the editor (File > Version history). There is no undo over MCP; to reverse a call, Claude applies the inverse edits. |
get_scene_screenshot | Render the scene server-side (real headless WebGL) and hand Claude an actual image of it, its "eyes" (see below). atSeconds takes the shot at a chosen moment of the scene's animation, so a door can be checked mid-swing. |
probe_lighting | What is lighting one point: every lamp in reach with its contribution, the sun or the sky, and the ambient fill named, so a surface that reads lit for no reason gets its reason (see below). |
probe_framing | Is a camera a good photograph: where its subject lands on the thirds, where the horizon cuts the frame, how high the eye stands, whether anything is in the lens, and whether a way out is in view, read at a desktop and a phone screen, with one suggested pose to fix it. A page's own arrival camera is judged as a page: the whole card, most of the width, at an angle its text still reads. Three or more equal things in frame (seven frames along a lawn) count as one subject, framed as a single arrangement. See Design help. |
design_review | A designer's critique of a scene or one page against its brief (what it is for): the palette and whether it serves the intent and mood, text contrast, heading hierarchy, every arrival shot, wayfinding, the light and motion, then the five fixes that matter most for that intent, each with why and a ready edit. See Design help. |
publish_scene | Publish to a public, shareable URL. Two steps on purpose: see below. |
generate_3d / generate_material ⚡ | Generate assets (paid, and only when a fal key is set). Local server only: the local server lists them when THREEDCMS_FAL_KEY is set, and the hosted connector never offers them, because a connected app's sign-in does not cover spending your credits. Like the in-editor confirm card, the first call quotes the price in credits and waits for a yes, and the quote reads the prompt: "photorealistic" or "intricate" runs at the high tier, so the 30 you approve is the 30 that is charged. |
The first thirteen are the core tools, and they're what the hosted connector offers.
Every result includes an editorUrl: open it while signed in to see and keep editing the scene in 3dCMS.
Quick phrases
| Say… | …and Claude |
|---|---|
| "list my scenes" | runs list_scenes |
| "make a scene called X with…" | creates it and builds it |
| "in <scene>, add / move / recolor…" | reads the scene, then edits it |
| "publish <scene>" | gives you a public 3D link |
Working on a scene too big to read
A few hundred nodes is more than a single tool result can carry, and get_scene used to return everything or nothing, so a scene Claude was perfectly able to build could become one it could no longer read. Three things fixed that, and they matter to you because they're what keeps a large scene editable by chat:
- You can orient without pulling the scene. Every read comes back with a count of nodes by type and how many matched, computed before anything is trimmed. Claude can narrow by type, name, or parent, and page through the rest; an unfiltered read of a huge scene is capped with a note rather than failing.
- Edits report what they created. An
apply_opsbatch hands back the id, type and name of every node it added, so the follow-up tweak goes straight to those objects instead of re-reading the whole tree to find them. - One edit can hit many objects. Update, material assignment and delete take a selector (by type, name prefix, name fragment, primitive, or parent) instead of a single id, which is how "give all sixty scattered trees a darker bark" stays one operation. Scattered copies are named after their source, so a scattered set is selectable as a set. A selector that matches nothing rejects the batch rather than silently doing nothing, and one match failing a safety check rolls the whole batch back, the same all-or-nothing promise a single edit makes.
- Materials say who wears them. Each material in a
get_sceneread carriesusedBy(a count and the first three node ids) orunused: true, so a scene that has minted a hundred materials over a long build can tell its orphans from its workhorses, and "every wearer of this material" is a selector (where.materialAssetId) rather than a hunt.
Texturing with what you already own
list_assets searches your asset library by label and keyword, every term having to match, so "weathered wood" narrows rather than returning everything wooden. Ask for "floor this room with one of my wood textures" and Claude finds the image, takes its address, and binds it, instead of generating a new material you'd pay for. Generated PBR sets are stored as separate images sharing a name prefix and ending in the map name (Base Color, Normal, Roughness), so a whole set can be rebound at once, though a base colour alone already reads as a texture. Each image also reports which map it is (base colour, normal, roughness and so on) and which set it belongs to, so Claude binds a set by its set id and places each file in the right slot instead of guessing from file sizes. A placed model reads back with its real bounds and size, so a picture frame can be fitted to a photo without a screenshot.
A bound set can also be tiled and animated, which is what makes ground and water buildable by chat: a repeat count (one number, or one per axis) and either a scroll or a flipbook (columns, rows, fps, and optionally frameCount for a sheet with empty cells and loop false for a one-shot), applied to every channel of the set together so an albedo tiled forty times never ends up beside a normal tiled once. A scrolling normal map on a large plane is the flowing-water recipe. Both are part of the material's identity, so asking for the same surface at a different tiling mints a new material rather than silently handing back the old one. A set can also be offset (a shift in tile units), which is how two pieces of one wall keep their wallpaper continuous across the seam instead of doubling the pattern at every join. Or skip the arithmetic: a world scale (metres per tile, in place of a repeat count) is turned into the right repeat for whatever the material lands on, from that piece's own dimensions and its scale in the world (its own times its parents'), so brick every 40 cm reads the same on a 2 m wall and a 12 m one, and a selector that re-textures sixty pieces sizes each by its own dimensions. A box has one repeat for three face sizes, so its largest face is the one sized right; an imported model keeps the tiling its file baked and is refused. And the seam arithmetic can be skipped too: ask for the offset from the world and each piece's pattern is phased to where the piece actually stands (its face's origin over the tile size), so two pieces of one wall carry the bricks straight across their join whatever their widths, and a selector phases every match by its own place. Box and plane only; each distinct phase is its own material (one texture upload is shared by every tiling and phase of a file), so it is for the pieces of a seam rather than every brick of a city. Both the tile count and the phase are worked out once, when the material is bound: a piece moved, resized or duplicated afterwards keeps the repeat and phase it was bound with, so re-assign the pieces you moved (get_scene shows each material's repeat and offset). A seventh map, ao, is a separate ambient-occlusion file for a set that ships one (a packed ORMH already carries it, so the two together are refused). A sixth map, emissive, names the pixels that glow (the lit windows of a facade, a screen, a neon tube, the cracks in lava): it is added as light on top of the surface, so it reads in the dark and under any time of day, with an intensity from 0 to 16 where 1 is the picture as painted. The same member binds itself when a texture set with an emissive file is dragged onto an object in the editor. A packed file (the ormh member every library set ships, with occlusion, roughness, metalness and height in its four colour channels, or a three-channel glTF orm) binds on its own slot and is split into its channels for you, the same graph the Materials window builds from a textures folder, so a library set is albedo + normal + ormh and nothing has to be mis-bound as roughness. A packed file and the separate map it packs cannot both be named, and reading a scene back lists what each textured material binds, packed file included. A picture with a transparent background (the library's gravestones, a cat, a ghost) used to draw its empty pixels as a solid card; alphaMode on the same member fixes that: mask is a hard cut-out whose transparent pixels are not drawn and cast no shadow, blend keeps soft see-through edges, and opaque is the default. It cuts by the alpha of the baseColor image, so that url has to be beside it, and it is part of the material's identity: a cut-out and a solid card of one picture are two materials. The editor's texture drop sets the same mode on its own when it sees alpha in the file.
When there is nothing to bind, Claude does not have to settle for a flat colour: the same fourteen material presets the Materials window offers (brushed metal, frosted ice, rough stone, gold leaf and the rest) are one word away on any add or material assignment, optionally tinted, so a first pass reads as a place rather than a grey box.
Building in bulk got cheaper too. A duplicate can be placed, named and parented in the same operation, and given a count and a step so sixteen identical wall units four metres apart are one operation, not forty-eight. A delete takes the same selector update does, removing each match with everything under it, with a lower default ceiling because it is the destructive one. And a page can drop its white card (showPanel: false) while its content and links still render, for a page that is a room or an anchor rather than a billboard.
A link sign can wear a texture too, plus a roughness, and can be told to cast a shadow, which turns a button from an overlay floating in front of a board into something that reads as sitting on its face. Those three are reachable through Claude and the Assistant only; the Properties panel still offers the solid, ghost and text-only styles.
Sizes read back as they are written
A primitive's dimensions live under one set of names in both directions. get_scene reports each node's size under exactly the keys data takes (a box {width, height, depth}, a plane {width, height}, a sphere {radius}, a cylinder {radius, height}, a torus {radius, tube}), so a size Claude reads is a size it can write straight back in an update's data. There is no size, w, h or d key: writing one is refused by name, with the primitive's real keys in the answer, instead of being stored where nothing reads it and reported as done. An update only takes the fields it can act on (name, visibility, position, rotation, scale, pivot and data); a dimension or a material key sent anywhere else is pointed at the right place rather than silently ignored.
Motion, viewpoints and shadows by chat
Three things the editor sets with a panel are set here with a field. A motion path (Following a path) is data.animation.path on the rider: curveId (a nurbs node, or its same-batch tempId, so "make the track, make the car, put the car on the track" is one call), facing tangent | target | fixed, forward z | -z | x | -x, up, target for a facing of target, offset 0 to 0.95, reverse, and facingEase seconds for the turn from the placed heading; easing beside it paces the ride. A typo'd facing, a box named as the curve, a curve inside the rider's own group or a webpage as the rider is refused by name rather than ridden wrong. get_scene reads a rider back as motionPath with a ride block. A curve something rides is a helper on the published site, registered for the ride and not drawn (a swing arc does not show as a green spline across the door); data.renderCurve: true on the curve draws it anyway, and the editor always shows it. A camera's legs (viewpoints) are data.viaCameraIds on the destination camera, the ordered viewpoints a flight passes through so it threads a doorway instead of a wall; up to eight, and a leg the visitor is already past is skipped. A viewpoint's own pacing is data.travelScale (0.25 to 4) on the camera: it multiplies the scene's travel scale for every flight that ends there, whether a page arrival, a sign or a #view link, so 2 is a slow reveal into a hero room and 0.5 a quick hop. It sits on the camera rather than the link because a page arrival never has a link at hand. A light's shadow map is data.shadowMapSize 512 | 1024 | 2048 (a point light caps at 1024), priced by its texels on the weight meter.
Publishing asks first
publish_scene won't publish on its first call. Ask Claude to publish and it gets back a fixed notice of what publishing means (the scene becomes public at a stable address, it is submitted to search engines, it is listed on Explore and your profile, and a first publish makes your creator name permanent) and has to call again with confirm: true before anything goes live. One more gate sits behind that: if the publish would replace a live site (its slug already belongs to another published scene), even a confirmed call stops, names the site, and goes ahead only on a further call with confirmReplace: true, so a retried publish can no longer overwrite a live site by accident. So you always get told before your work becomes public, in the same way the generation tools quote their price before spending credits.
Two optional fields shape where it lands and how you share it. siteSlug publishes onto one of your own existing sites by its slug (the part after /sites/ in its URL) instead of the scene's own, so "put this version on my launch site" is one ask. A slug that isn't yours, or doesn't exist, is refused as not found; a site currently showing a different scene gets the same replace-first stop as above, naming the site, its new public title and whether it is your homepage, plus a replaceToken good for ten minutes that the confirming call must pass back (a plain yes is refused, so an old answer cannot replace a site). The replaced site gets a fresh share card of the new scene. Once it goes through, later publishes of this scene go to that site, and the site the scene used to publish to stays live as it was, just no longer paired with it. shortLink: true also returns a shortUrl, the same 3dcms.info/s/<code> short link the Share dialog mints (one stable code per site), ready for a post or a QR code. Asked for on a scene whose site is already live, the short link comes back straight away in the preview, with no republish.
The reason is that publishing isn't fully reversible: the URL is submitted to search engines the moment it goes live, so it can surface in results even after you take the page down. Taking it down is one click and undoable; an un-send isn't a thing. Re-publishing the same scene just updates the site it already has, and the URL stays put.
Whole pages in one step
When you ask for a page (a heading, a paragraph, and a few navigation links), Claude uses the make_page composite op, which builds the entire page surface (hero heading, body paragraph, and clickable "sign" links) in a single operation instead of assembling it from a dozen primitives. It's faster and lays pages out consistently, so a multi-page site comes together in a handful of ops. You don't invoke it by name: just ask, e.g. "add a Contact page with a heading, a short intro, and a link back home."
For a scene with several rooms, Claude also binds each room to its page (data.zonePageId on the group that holds the room, since the binding is inherited by everything under it, plus data.zoneRadius for something that has to stay visible from the corridor). A bound room switches off while the visitor is elsewhere, and (the part that's invisible until it bites) each room gets its own budget of four shadow-casting lights instead of the first rooms spending all four. See scoping a room to its neighborhood.
Flowcharts & mind maps in one step
Ask for a diagram (a process, a pipeline, an org chart, a mind map) and Claude reaches for the make_diagram composite op. You supply the ideas and how they connect; it builds a labelled card for each one, a connector for each link, and works out the layout itself, so no box is placed by hand. Two shapes are available: layered (the default, rows stacking downward from whatever nothing points at, right for processes, pipelines, and org charts) and radial (one idea in the middle with rings of children around it, right for mind maps). Feedback loops and disconnected ideas are drawn, not rejected.
Every card is tap-to-travel, which is the thing a 3D diagram does that a flat image can't: publish the scene and the link is the diagram. A visitor taps a box and flies over to read it up close. Try: "make a mind map of my product launch with five branches" or "draw the review → fix → build → review loop as a flowchart."
Printed surfaces and picture frames
It is a heavy call (a real headless browser renders the surface on the server), so it is rationed: one compose at a time for everyone, and 60 a day per account; a busy or over-budget answer says in words when to try again.
compose_surface starts from a base (a texture set from your library, tiled or stretched, explicit maps, or a flat colour, optionally tinted) and paints layers on it in order: text in a typewriter, sans or horror face, image decals, stains, blood spatter with drips, edge grime and flat fills. Ink and spatter sink into the base's relief and read slightly glossier than it, so a light raking across a canvas shows the weave through the lettering. The result is either a thin panel model (a phone copy is made for you, and like any model it waits for its room to load) or a material set to bind onto something you already have. Claude gets a small preview image back to check before placing it.
The two tools work together for a gallery: Claude runs measure on a frame to read its opening, composes a panel about 0.2 m larger both ways, and seats it just behind the frame's front so the edge tucks behind the lip. Try: "In my gallery, put this quote on a canvas in the big frame on the back wall."
Claude can see what it builds
Building a 3D scene blind is hard. With get_scene_screenshot, Claude renders your scene server-side with real WebGL and gets back an actual image, then judges it like an art director (floating objects? unreadable text? an empty horizon? bad spacing?) and fixes what's off with another apply_ops. It caps itself at a couple of look-and-fix rounds per request so it doesn't loop forever. The upshot: scenes come out looking intentional, not just structurally correct. (The default is one view at 640 px, which reads captions and renders in time. A call answers within about 85 seconds: the image, or "still rendering, retry in N s", and Claude keeps building meanwhile, then asks again with the same request and gets the finished image from the cache at no extra cost. A renderer busy with someone else's shot answers the same way, a wait in seconds rather than an error, and a shot that never got to render is not charged to the budget. Each view has a time budget Claude can raise for a heavy scene, up to three minutes; repeat shots of an unchanged scene are cached and free.)
Server-side renders come out of a daily budget, because each one occupies a real render slot on the server for ten seconds or so: 10 a day on the free plan, rising to 100, 300, and 1,000 on the paid tiers. Run out and the tool answers with a plain "budget used up, resets tomorrow (UTC)" instead of failing oddly. Claude can carry on building; it just can't look again until then. The publish-time social card is captured in your own browser and never touches this budget.
Setting the scene, not just its objects
Every other operation edits objects. The settings op edits the scene, and it is the only way to reach any of it: ask for a sunset, fog, or a post-processing look and this is what runs.
| Section | What it sets |
|---|---|
viewport | The published look and how a visitor's device draws it: bloom, vignette, depth of field, colour grading, glitch, grain, lens distortion, chromatic aberration, god rays, lens flare, the colour LUT and tone mapping, plus draw distance, level-of-detail, the loading placeholder, touch and joystick feel, how camera flights ease, how long they take and how far they arc, the neighborhood fade, where the reaction bar and guestbook live, and the share badge (shareBadge true, with shareBadgeLabel up to 40 characters and shareBadgeColor as #rrggbb): a round share button pinned to the bottom-right of the published view, the screen-space sibling of a #share sign. Its bottom-left twin is the action badge (ctaBadge true, ctaBadgeLabel up to 24 characters, an optional ctaBadgeRim line up to 40, ctaBadgeColor, and a required ctaBadgeHref: https, mailto or a /path), and badgeTheme light gives both badges a white disc for a light scene. |
lighting | The sun and the sky: live or a pinned time of day (a preset such as sunset is re-derived for the current date, so it stays sunset all year; a clock time drifts with the season), the studio sun angle and colour, the ambient fill (env.intensity, which in time mode scales the sky's own fill), and the moonlight (moon.intensity, 0 turns the moon's light off while the disc still shows and the sky's faint night fill stays). Writing minutes pins a clock time and clears any stored preset. |
environment | Fog and the background colour: fog colour, near and far, linear or exponential. |
audio | The scene's audio bus: master and ambient levels, and a mute. |
ui | Editor preferences stored on the scene. They travel with it and change nothing a visitor sees. |
Sections merge, so setting one field leaves its neighbours alone. The op also takes the scene name and an activeCameraId. A rename renames the saved scene too, so a scene renamed before its first publish goes live at the new name's address; a site already published keeps its address, and the reply says so. Two things it deliberately cannot touch: custom branding, which is a paid entitlement checked when it is saved, and a custom LUT's file address, which is a security check rather than a setting.
Every value is range-checked. An unknown field or an out-of-range number rejects the whole batch with a message naming the field, instead of quietly clamping it and reporting success for something you did not ask for.
And it reads back: get_scene carries a settings block with every one of these fields the scene holds, plus settings.sky, one sentence on what actually lights the scene. Publishing a scene whose sky is still live (following each visitor's own clock) says so in the publish result, with the op that pins it.
Why is this lit?
Ambient fill has no source and no falloff: it lifts every surface evenly, and a pale ceiling over dark walls can read as a light fixture that does not exist. Deleting lamps one by one does not find it. probe_lighting does: give it a point in the world (and the node it sits on) and it answers with every lamp in reach ranked by contribution, a reason for each one that does not reach (out of range, outside the cone), the sun or the sky, and the ambient term by name, with the surface's albedo beside it. A scene meant to be dark sets lighting.env.intensity to 0 and lights itself with lamps; the lint says so too when the studio sun is off and the ambient is not.
A camera can ride a curve: give a camera node a motion path and every flight that lands on it becomes a ride along that curve, looking along it, at a point, or the way it arrived, with data.rideBreak deciding what hands control back: any (the default, any input), move (only a drag, a wheel or a movement key, so a tap on a sign still navigates) or none (nothing; a loop set to once ends on its own). On an open curve use once or pingpong, or close the curve: a plain loop cuts back to the start every lap. The editor's Ride section on the camera holds the same fields, and its Play button shows the camera marker riding, which is the flight preview.
Two more that came out of the same room: a downward-only bulb (a lamp that fills a room without washing its ceiling) is a spot with angle 1.55 and no rotation, and a lamp whose range reaches a ceiling above it is named by the lint with the distance. A colour material can glow by hex (emissive + emissiveIntensity beside color), so a lampshade needs no texture; a materialPreset beside a color takes that colour, keeping its own light-to-dark contrast. A where selector reaches a whole room with underId, or every wearer of a material with materialAssetId; pivot on an add is the hinge; a per-node lint warning you disagree with goes quiet with data.lintMute.
A pinned time needs pinning. Asking for sunset sets the preset, but the sky only stops following the viewer's own clock when the source is set to fixed too.
A low sun needs fill. At sunset almost nothing is lit directly, so a dark scene renders nearly black; raising
lighting.env.intensity brings it back. In studio mode it is the reflection fill; in time mode it scales the sky's own fill (about 0.2 by night to 0.9 by day at the default 0.55), never the sun or moon. At 0 only the sun, the moon and your lamps light anything, so a lamp-only night scene is a midnight pin with env 0 and moon 0.Live scene lint
Every apply_ops result can carry advisory warnings: a lightweight scene lint that flags common mistakes like content with no real ground or backdrop under it (a "floor" left standing on edge counts as no floor), "orphan" web blocks floating off a page, text that's too small to read, stacked duplicates, an overcrowded page, a lamp fixture parked in its own beam (named with both clearances in metres), a backdrop colour the current lighting mode will never show, an object bound to a different neighborhood than the group it sits in (almost always a copy that carried the original room's binding across, and it vanishes in the room it is actually in), or an image hot-linked from another website (it renders for the signed-in author and shows as empty for every visitor). Three more came out of a dark room that would not go dark: ambient fill left on in a studio scene whose sun is off (it lifts every surface evenly, pale ones most), a point light or an upward spot whose range reaches a ceiling above it (named with the distance and the two fixes), a page whose white card is still on with a room's worth of geometry under it (showPanel: false keeps the text, links and children), a large plane facing a shadow lamp with content behind it (a plane blocks light from behind only; a thin box has both faces), and a camera ride looping an open curve. When a room has more than four shadow-casting lights, a warning names the ones past the fourth, which cast nothing, and says the fix: bind each room's group with data.zonePageId so the budget is counted per room, or turn the extras' shadows off. A floor laid above the content it should hold up is named the same way, with the edit that sets it down at ground level. The edits still apply; the warnings just tell Claude what to tidy up in its next batch. A per-node warning you have weighed and disagree with goes quiet with data.lintMute: true on that node (every rule about one node honours it); scene-level warnings stay. Six warnings show at a time; when more fire, the last entry counts the rest. The same lint runs behind the in-editor Assistant, so guidance is identical wherever you build.
The hot-linked-image half of that lint also runs at publish time, on both publish paths, so it reaches an author who pasted a URL into Properties by hand and never went near a tool call. A successful publish_scene can therefore come back with a warning naming the nodes, joined onto any scene-weight warning rather than replacing it. One thing it deliberately never warns about: an image slot a template personalizes, whose address is a placeholder the clone replaces wholesale, so the advice would be about a mechanism rather than a mistake.
That mechanism is now visible from the tools. get_scene lists a node's personalize marker when it has one: which owner value (name, email, photo) the node's text or picture becomes when a template is cloned. It is a real data key, so a batch can write it back as-is. When the scene you are editing is itself a gallery template, every apply_ops result also carries TEMPLATE warnings for the two edits that break personalization silently: a marked node whose text no longer holds the author's own value (so every clone would keep the template's words as they are; keep the author's name or email in the text, or write the {{ownerName}} or {{ownerEmail}} token), and author text that carries no marker at all (so the author's name or email would survive into every clone). The edits still apply; the warnings say what to put back.
Checked on arrival
Beyond the advisory lint, the ops route refuses or corrects a few things outright, so a typo cannot be accepted and quietly do nothing. A color on 3D text, a light, a cable, a shadow pool, a backdrop, a particle emitter or a curve writes that node's own shown colour (it used to mint a material nothing rendered), and any material field on a group, camera, web block or force field is refused by name. The documented ground recipe sits at position: [0, 0, 0], and a floor that lands above most of the content draws a warning. Animation tracks and taps are validated as they arrive (track names, key shape, two to sixteen keys with t from 0 to 1, a real action and target on a tap), and get_scene reports animated: true only for motion that survived. make_page signs accept linkedPageId and linkedCameraId, so a page's way out points somewhere. A per-node directional light (the old lightType) is refused with a pointer to the settings op, where the sun and sky actually live.
apply_ops calls, checking the scene (and a screenshot) between them. Try: "In my Gallery scene, add a row of five 1-meter cubes 2 m apart, then group them."