Blender Control
Blender (/Applications/Blender.app) is scriptable in Python against bpy, bmesh and
mathutils. Drive it through the Blender Bridge — a script running inside a running
Blender GUI that executes whatever build script is POSTed to 127.0.0.1:8736. Because it
runs in the live session on the main thread, bpy.data writes are legal, the viewport
updates as you build, and viewport screenshots work. Everything documented here was executed
and verified on Blender 5.2.0 LTS (bundled Python 3.13).
- scripts/blender-bridge.py — the bridge to run inside Blender.
- scripts/blender-send.sh — send a
.py file (or -c 'inline')
and print its captured output; --ping checks it's up, --state dumps the scene as JSON.
- scripts/example-product-shot.py — model → material →
three-point light → camera → measure → render.
- scripts/example-animated-logo.py — text → keyframes →
geometry-nodes scatter → frame sequence → video.
Read references/gotchas.md before writing anything. Blender's
API changed substantially in 5.x and most of what a model has memorised — action.fcurves,
scene.node_tree, mod["Socket_2"], BLENDER_EEVEE_NEXT — is now wrong.
The control loop
User starts the bridge (one-time). This cannot be done remotely — if
bash scripts/blender-send.sh --ping gets no answer, ask the user to do one of:
- copy scripts/blender-bridge.py to
~/Library/Application Support/Blender/5.2/scripts/startup/blender_bridge.py and
restart Blender — it then starts automatically on every launch (recommended), or
- open the Scripting workspace, paste the file into the text editor, press
Run Script (⌥P) — lasts for that session, or
- install it as an add-on via Preferences ▸ Add-ons ▸ Install.
A successful ping returns
{"ok": true, "bridge": "blender", "version": "5.2.0 LTS", "file": null, ...}.
Look before you build: bash scripts/blender-send.sh --state returns objects and
types, collections, materials, node groups, frame range, resolution, engine and whether
the file has unsaved changes — cheaper than writing a script to ask.
Write a build script to the scratchpad, starting from the cheatsheet below and the
right reference file.
Send it: OUT=/path/to/outdir bash scripts/blender-send.sh /path/build.py. The bridge
runs it in the live session and returns the script's captured stdout; on error it
returns the traceback and the sender exits non-zero. BLENDER_SEND_TIMEOUT=900
(seconds) for heavy renders and bakes.
Feedback: returned stdout first; metrics(...) for structured geometry checks;
snapshot(...) for a viewport PNG to Read; render(...) for the real thing.
Iterate: inspect, fix, re-send. Scripts must be re-runnable — build inside
stage("name") so a re-send replaces its own output instead of stacking duplicates.
Reference routing
| Task |
Read |
| Anything, before you start |
gotchas.md |
| Data-blocks, transforms, collections, depsgraph, scenes |
api-reference.md |
| bmesh, modifiers, curves, text, UVs, booleans |
modeling.md |
| Materials, shader nodes, textures, world/HDRI, lights, cameras |
shading.md |
| Procedural geometry, scattering, instancing, fields, zones |
geometry-nodes.md |
| Keyframes, F-curves, drivers, NLA, armatures, IK, shape keys |
animation-rigging.md |
| Rigid body, cloth, soft body, particles, hair, fluid, bakes |
simulation.md |
| EEVEE/Cycles settings, passes, compositor, output, video |
rendering.md |
| 2D / toon linework, Line Art |
grease-pencil.md |
| Editing clips, titles, transitions, final cut |
vse.md |
| glTF, FBX, USD, Alembic, OBJ, STL, .blend append/link |
io-formats.md |
Injected namespace
Pre-imported: bpy, bmesh, mathutils, Vector, Matrix, Euler, Quaternion,
math, os, json, plus OUT (from $OUT, also os.environ["OUT"]) and these helpers:
| Helper |
Does |
stage(name) |
get-or-recreate a named collection, make it active — makes re-sends idempotent |
sync() |
view_layer.update(); required before reading matrix_world |
evaluated(ob) |
the depsgraph-evaluated object (modifier / geometry-nodes result) |
frame(n) |
frame_set(n) + depsgraph update |
metrics(objs, path=) |
dict + JSON: counts, verts/tris, world bbox, materials, frame range |
snapshot(path, view=, shading=, fit=) |
viewport PNG (fast visual check) |
render(path, engine=, samples=, ...) |
real EEVEE/Cycles still; restores every setting |
frame_view(objs, view=) |
aim the viewport (ISO/FRONT/TOP/CAMERA/…) |
world_bounds(objs) |
world-space (min, max) |
fcurves(ob) / fcurve(ob, path, i) / channelbag(ob) |
slotted-action F-curve access |
ui_override(area) |
context override for the few bpy.ops that need an editor |
Cheatsheet
Units are metres and radians. Prefer the data API; bpy.ops is for mode changes,
smart_project, rigidbody.*, nla.bake, and importers/exporters.
Build geometry (re-runnable)
coll = stage("hero_v1") # owns its own output; safe to re-send
me = bpy.data.meshes.new("Body")
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=2.0)
bmesh.ops.bevel(bm, geom=list(bm.edges), offset=0.06, segments=3, affect="EDGES")
bm.to_mesh(me); bm.free()
ob = bpy.data.objects.new("Body", me)
coll.objects.link(ob)
ob.location = (0, 0, 1)
sync() # before ANY matrix_world read
Material
m = bpy.data.materials.new("Shell")
m.use_nodes = True
b = m.node_tree.nodes["Principled BSDF"]
b.inputs["Base Color"].default_value = (0.75, 0.2, 0.15, 1.0)
b.inputs["Roughness"].default_value = 0.35
b.inputs["Metallic"].default_value = 0.0 # no "Specular"/"Emission" inputs since 4.0
ob.data.materials.append(m)
Camera aimed at the subject
cd = bpy.data.cameras.new("Cam"); cd.lens = 50
cam = bpy.data.objects.new("Cam", cd); coll.objects.link(cam)
cam.location = (6, -6, 4)
cam.rotation_euler = (Vector((0, 0, 1)) - cam.location).to_track_quat("-Z", "Y").to_euler()
bpy.context.scene.camera = cam
Feedback
m = metrics([ob], path=OUT + "/metrics.json")
print("METRICS", json.dumps(m)) # comes straight back through the sender
print(snapshot(OUT + "/check.png", view="ISO"))
print(render(OUT + "/hero.png", engine="BLENDER_EEVEE", samples=64,
width=1280, height=720))
Keyframes
for f, z in ((1, 0.0), (24, 3.0), (48, 0.0)):
ob.location.z = z
ob.keyframe_insert("location", index=2, frame=f)
for kp in fcurves(ob)[0].keyframe_points: # NOT action.fcurves — that is gone
kp.interpolation = "BEZIER"
kp.easing = "EASE_IN_OUT"
Export
bpy.context.view_layer.objects.active = ob; ob.select_set(True)
bpy.ops.export_scene.gltf(filepath=OUT + "/hero.glb", export_format="GLB",
use_selection=True)
bpy.ops.wm.obj_export(filepath=OUT + "/hero.obj", export_selected_objects=True)
bpy.ops.wm.save_as_mainfile(filepath=OUT + "/hero.blend", copy=True)
Verification
- Returned stdout is the immediate signal —
print(...) comes straight back.
metrics(...) is the structured check (object/vert/tri counts, world bbox, materials).
Write it to $OUT/metrics.json and Read it to confirm geometry without eyeballing.
snapshot(...) is the fast visual check — a viewport OpenGL PNG, no full render.
shading="RENDERED" previews materials and lights.
render(...) for the deliverable. It raises a diagnostic RuntimeError if Blender
reported success but wrote nothing.
- Hand-off: a
.glb/.blend in $OUT opens in whatever the user already has.
Security
Installing this skill means running a code-execution server on the user's machine. Say
so before asking them to start the bridge.
- The bridge binds
127.0.0.1:8736 (BLENDER_BRIDGE_PORT) and executes any Python POSTed
to /run inside the live session — the user's privileges, the user's open file. Requests
carry no authentication: every local process, and every other user on a shared
machine, can drive Blender through it.
- Web pages cannot. Requests carrying an
Origin header or a cross-site
Sec-Fetch-Site are rejected with 403, so a page in the user's browser can't reach the
bridge. That check is the only gate — there is no token.
- The install choice decides how long the port stays open. Scripting workspace ▸ Run
Script lasts until Blender quits. The
scripts/startup/ install is recommended above for
convenience, but it makes every Blender launch listen, whether or not an agent is
driving it — offer the trade-off rather than assuming it.
- To stop the bridge, quit Blender. There is no remote shutdown; the port is released
with the process.
Safety
- The open file may hold unsaved work. Never call
bpy.ops.wm.read_homefile() or
bpy.data.batch_remove without asking. stage() is the non-destructive default; it
isolates geometry but not scene-level settings.
- Renders, bakes and sims block the main thread — the UI freezes until they finish.
Iterate small, and warn the user before anything long.
- Save into
$OUT, not over the user's .blend; save_as_mainfile(..., copy=True) leaves
the live session pointed at their own file.
1---2name: blender3description: Remote-control a running Blender by Python script through a small local bridge — build and edit 3D scenes live: mesh and curve modeling with bmesh and modifiers, materials and shader node graphs, geometry nodes, lighting and cameras, keyframe animation, rigging, physics simulation, Grease Pencil, the video sequencer, and EEVEE/Cycles rendering, then measure the result, grab viewport screenshots, and export glTF / FBX / USD / Alembic / OBJ / STL. Use whenever the user wants to create or edit a 3D scene, model, animation, product shot, or .blend file, render an image or video, convert or export a 3D asset, or says "Blender", "3D model", "render this", "make a 3D animation", "Blender scene", "export to glTF" — even if they don't mention scripting.4---56# Blender Control78Blender (`/Applications/Blender.app`) is scriptable in **Python** against `bpy`, `bmesh` and9`mathutils`. Drive it through the **Blender Bridge** — a script running inside a *running*10Blender GUI that executes whatever build script is POSTed to `127.0.0.1:8736`. Because it11runs in the live session on the main thread, `bpy.data` writes are legal, the viewport12updates as you build, and viewport screenshots work. Everything documented here was executed13and verified on **Blender 5.2.0 LTS** (bundled Python 3.13).1415- [scripts/blender-bridge.py](scripts/blender-bridge.py) — the bridge to run inside Blender.16- [scripts/blender-send.sh](scripts/blender-send.sh) — send a `.py` file (or `-c 'inline'`)17 and print its captured output; `--ping` checks it's up, `--state` dumps the scene as JSON.18- [scripts/example-product-shot.py](scripts/example-product-shot.py) — model → material →19 three-point light → camera → measure → render.20- [scripts/example-animated-logo.py](scripts/example-animated-logo.py) — text → keyframes →21 geometry-nodes scatter → frame sequence → video.2223**Read [references/gotchas.md](references/gotchas.md) before writing anything.** Blender's24API changed substantially in 5.x and most of what a model has memorised — `action.fcurves`,25`scene.node_tree`, `mod["Socket_2"]`, `BLENDER_EEVEE_NEXT` — is now wrong.2627## The control loop28291. **User starts the bridge** (one-time). This cannot be done remotely — if30 `bash scripts/blender-send.sh --ping` gets no answer, ask the user to do one of:31 - copy [scripts/blender-bridge.py](scripts/blender-bridge.py) to32 `~/Library/Application Support/Blender/5.2/scripts/startup/blender_bridge.py` and33 restart Blender — it then starts automatically on every launch (**recommended**), or34 - open the **Scripting** workspace, paste the file into the text editor, press35 **Run Script** (⌥P) — lasts for that session, or36 - install it as an add-on via Preferences ▸ Add-ons ▸ Install.3738 A successful ping returns39 `{"ok": true, "bridge": "blender", "version": "5.2.0 LTS", "file": null, ...}`.402. **Look before you build**: `bash scripts/blender-send.sh --state` returns objects and41 types, collections, materials, node groups, frame range, resolution, engine and whether42 the file has unsaved changes — cheaper than writing a script to ask.433. **Write a build script** to the scratchpad, starting from the cheatsheet below and the44 right reference file.454. **Send it**: `OUT=/path/to/outdir bash scripts/blender-send.sh /path/build.py`. The bridge46 runs it in the live session and returns the script's **captured stdout**; on error it47 returns the **traceback** and the sender exits non-zero. `BLENDER_SEND_TIMEOUT=900`48 (seconds) for heavy renders and bakes.495. **Feedback**: returned stdout first; `metrics(...)` for structured geometry checks;50 `snapshot(...)` for a viewport PNG to Read; `render(...)` for the real thing.516. **Iterate**: inspect, fix, re-send. Scripts must be **re-runnable** — build inside52 `stage("name")` so a re-send replaces its own output instead of stacking duplicates.5354## Reference routing5556| Task | Read |57|---|---|58| Anything, before you start | [gotchas.md](references/gotchas.md) |59| Data-blocks, transforms, collections, depsgraph, scenes | [api-reference.md](references/api-reference.md) |60| bmesh, modifiers, curves, text, UVs, booleans | [modeling.md](references/modeling.md) |61| Materials, shader nodes, textures, world/HDRI, lights, cameras | [shading.md](references/shading.md) |62| Procedural geometry, scattering, instancing, fields, zones | [geometry-nodes.md](references/geometry-nodes.md) |63| Keyframes, F-curves, drivers, NLA, armatures, IK, shape keys | [animation-rigging.md](references/animation-rigging.md) |64| Rigid body, cloth, soft body, particles, hair, fluid, bakes | [simulation.md](references/simulation.md) |65| EEVEE/Cycles settings, passes, compositor, output, video | [rendering.md](references/rendering.md) |66| 2D / toon linework, Line Art | [grease-pencil.md](references/grease-pencil.md) |67| Editing clips, titles, transitions, final cut | [vse.md](references/vse.md) |68| glTF, FBX, USD, Alembic, OBJ, STL, .blend append/link | [io-formats.md](references/io-formats.md) |6970## Injected namespace7172Pre-imported: `bpy`, `bmesh`, `mathutils`, `Vector`, `Matrix`, `Euler`, `Quaternion`,73`math`, `os`, `json`, plus `OUT` (from `$OUT`, also `os.environ["OUT"]`) and these helpers:7475| Helper | Does |76|---|---|77| `stage(name)` | get-or-recreate a named collection, make it active — makes re-sends idempotent |78| `sync()` | `view_layer.update()`; **required before reading `matrix_world`** |79| `evaluated(ob)` | the depsgraph-evaluated object (modifier / geometry-nodes result) |80| `frame(n)` | `frame_set(n)` + depsgraph update |81| `metrics(objs, path=)` | dict + JSON: counts, verts/tris, world bbox, materials, frame range |82| `snapshot(path, view=, shading=, fit=)` | viewport PNG (fast visual check) |83| `render(path, engine=, samples=, ...)` | real EEVEE/Cycles still; restores every setting |84| `frame_view(objs, view=)` | aim the viewport (`ISO`/`FRONT`/`TOP`/`CAMERA`/…) |85| `world_bounds(objs)` | world-space `(min, max)` |86| `fcurves(ob)` / `fcurve(ob, path, i)` / `channelbag(ob)` | slotted-action F-curve access |87| `ui_override(area)` | context override for the few `bpy.ops` that need an editor |8889## Cheatsheet9091Units are **metres** and **radians**. Prefer the data API; `bpy.ops` is for mode changes,92`smart_project`, `rigidbody.*`, `nla.bake`, and importers/exporters.9394### Build geometry (re-runnable)9596```python97coll = stage("hero_v1") # owns its own output; safe to re-send98me = bpy.data.meshes.new("Body")99bm = bmesh.new()100bmesh.ops.create_cube(bm, size=2.0)101bmesh.ops.bevel(bm, geom=list(bm.edges), offset=0.06, segments=3, affect="EDGES")102bm.to_mesh(me); bm.free()103ob = bpy.data.objects.new("Body", me)104coll.objects.link(ob)105ob.location = (0, 0, 1)106sync() # before ANY matrix_world read107```108109### Material110111```python112m = bpy.data.materials.new("Shell")113m.use_nodes = True114b = m.node_tree.nodes["Principled BSDF"]115b.inputs["Base Color"].default_value = (0.75, 0.2, 0.15, 1.0)116b.inputs["Roughness"].default_value = 0.35117b.inputs["Metallic"].default_value = 0.0 # no "Specular"/"Emission" inputs since 4.0118ob.data.materials.append(m)119```120121### Camera aimed at the subject122123```python124cd = bpy.data.cameras.new("Cam"); cd.lens = 50125cam = bpy.data.objects.new("Cam", cd); coll.objects.link(cam)126cam.location = (6, -6, 4)127cam.rotation_euler = (Vector((0, 0, 1)) - cam.location).to_track_quat("-Z", "Y").to_euler()128bpy.context.scene.camera = cam129```130131### Feedback132133```python134m = metrics([ob], path=OUT + "/metrics.json")135print("METRICS", json.dumps(m)) # comes straight back through the sender136print(snapshot(OUT + "/check.png", view="ISO"))137print(render(OUT + "/hero.png", engine="BLENDER_EEVEE", samples=64,138 width=1280, height=720))139```140141### Keyframes142143```python144for f, z in ((1, 0.0), (24, 3.0), (48, 0.0)):145 ob.location.z = z146 ob.keyframe_insert("location", index=2, frame=f)147for kp in fcurves(ob)[0].keyframe_points: # NOT action.fcurves — that is gone148 kp.interpolation = "BEZIER"149 kp.easing = "EASE_IN_OUT"150```151152### Export153154```python155bpy.context.view_layer.objects.active = ob; ob.select_set(True)156bpy.ops.export_scene.gltf(filepath=OUT + "/hero.glb", export_format="GLB",157 use_selection=True)158bpy.ops.wm.obj_export(filepath=OUT + "/hero.obj", export_selected_objects=True)159bpy.ops.wm.save_as_mainfile(filepath=OUT + "/hero.blend", copy=True)160```161162## Verification163164- **Returned stdout** is the immediate signal — `print(...)` comes straight back.165- **`metrics(...)`** is the structured check (object/vert/tri counts, world bbox, materials).166 Write it to `$OUT/metrics.json` and Read it to confirm geometry without eyeballing.167- **`snapshot(...)`** is the fast visual check — a viewport OpenGL PNG, no full render.168 `shading="RENDERED"` previews materials and lights.169- **`render(...)`** for the deliverable. It raises a diagnostic `RuntimeError` if Blender170 reported success but wrote nothing.171- **Hand-off**: a `.glb`/`.blend` in `$OUT` opens in whatever the user already has.172173## Security174175Installing this skill means running a **code-execution server** on the user's machine. Say176so before asking them to start the bridge.177178- The bridge binds `127.0.0.1:8736` (`BLENDER_BRIDGE_PORT`) and executes any Python POSTed179 to `/run` inside the live session — the user's privileges, the user's open file. Requests180 carry **no authentication**: every local process, and every other user on a shared181 machine, can drive Blender through it.182- Web pages **cannot**. Requests carrying an `Origin` header or a cross-site183 `Sec-Fetch-Site` are rejected with 403, so a page in the user's browser can't reach the184 bridge. That check is the only gate — there is no token.185- **The install choice decides how long the port stays open.** Scripting workspace ▸ Run186 Script lasts until Blender quits. The `scripts/startup/` install is recommended above for187 convenience, but it makes *every* Blender launch listen, whether or not an agent is188 driving it — offer the trade-off rather than assuming it.189- **To stop the bridge, quit Blender.** There is no remote shutdown; the port is released190 with the process.191192## Safety193194- The open file may hold **unsaved work**. Never call `bpy.ops.wm.read_homefile()` or195 `bpy.data.batch_remove` without asking. `stage()` is the non-destructive default; it196 isolates geometry but **not** scene-level settings.197- Renders, bakes and sims **block the main thread — the UI freezes** until they finish.198 Iterate small, and warn the user before anything long.199- Save into `$OUT`, not over the user's `.blend`; `save_as_mainfile(..., copy=True)` leaves200 the live session pointed at their own file.