# How to use Blender with AI coding agents

An agent built a Blender scene in Python and rendered it headlessly: 8 minutes on a CPU, 9.6 seconds on a cloud GPU. The setup, the costs, and the alternatives.

- Canonical: https://replicas.dev/resources/how-to-use-blender-with-ai-agents

Blender has a full Python API, `bpy`, and renders from the command line. That means a coding agent can build a scene and render it without touching the UI. The hard part is the render. Cycles, Blender's path tracer, wants a GPU, and Replicas workspaces don't have one.

So I had an agent in a Replicas workspace build a scene in Python, render it on the workspace CPU, then render the same file on six cloud GPUs with [GPU sandboxes](/blog/gpu-sandboxes). All the GPU runs behind this guide cost about $1.80 at Modal's list prices.

![A field of dark graphite hexagonal columns, a few glowing green and a few glass, rendered in Blender Cycles](/blog/blender-with-ai-agents-hero.jpg)

*The final frame at 1600×1000 and 512 samples: 8m 13s on the workspace CPU, 9.6s on an L40S.*

## Building the scene in Python

Everything in that frame comes from one 101-line script. The agent used `bmesh` to build 799 hexagonal columns with heights from a noise function, gave them three materials (graphite, green emission, glass), added three area lights and a camera with depth of field, and saved `scene.blend`:

```bash
blender -b --factory-startup -P scene.py
```

An agent can't watch a viewport, but it can read an image. So the loop was: edit the script, render a half-size preview at 32 samples on the CPU, look at the PNG, repeat. Each preview took about 10 seconds. The first one had blown-out white emitters and an almost flat field. Two rounds later it looked like this:

![A noisy half-resolution preview of the same scene](/blog/blender-with-ai-agents-cpu-preview.jpg)

*The 10-second CPU preview the agent checked before rendering the full frame.*

<details>
<summary>see the full scene script</summary>

```python
import bpy, bmesh, math, random, sys
from mathutils import Vector, Matrix, noise

argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
opt = {argv[i].lstrip("-"): argv[i + 1] for i in range(0, len(argv), 2)}
SAMPLES = int(opt.get("samples", 512))
PERCENT = int(opt.get("percent", 100))

bpy.ops.wm.read_factory_settings(use_empty=True)
scene = bpy.context.scene
random.seed(7)

def material(name, color, metallic=0.0, roughness=0.3, emission=None, strength=0.0, transmission=0.0):
    m = bpy.data.materials.new(name)
    m.use_nodes = True
    p = m.node_tree.nodes["Principled BSDF"]
    p.inputs["Base Color"].default_value = (*color, 1)
    p.inputs["Metallic"].default_value = metallic
    p.inputs["Roughness"].default_value = roughness
    p.inputs["Transmission Weight"].default_value = transmission
    if emission:
        p.inputs["Emission Color"].default_value = (*emission, 1)
        p.inputs["Emission Strength"].default_value = strength
    return m

mats = [
    material("Graphite", (0.03, 0.033, 0.032), metallic=0.7, roughness=0.38),
    material("Glow", (0.048, 0.83, 0.37), emission=(0.048, 0.83, 0.37), strength=3.5),
    material("Glass", (0.7, 1.0, 0.85), roughness=0.12, transmission=1.0),
]

bm = bmesh.new()
size, gap = 1.0, 0.06
for q in range(-16, 17):
    for r in range(-16, 17):
        x = size * 1.5 * q
        y = size * math.sqrt(3) * (r + q / 2)
        if math.hypot(x, y) > 26:
            continue
        n = noise.noise(Vector((x * 0.07, y * 0.07, 0.5)))
        h = 0.8 + 4.5 * (n + 1) ** 1.5 + random.random() * 0.8
        roll = random.random()
        idx = 1 if roll < 0.05 else 2 if roll < 0.11 else 0
        geom = bmesh.ops.create_cone(bm, cap_ends=True, segments=6, radius1=size - gap, radius2=size - gap, depth=h,
                                     matrix=Matrix.Translation((x, y, h / 2)))
        for f in {f for v in geom["verts"] for f in v.link_faces}:
            f.material_index = idx

mesh = bpy.data.meshes.new("Hexes")
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new("Hexes", mesh)
scene.collection.objects.link(obj)
for m in mats:
    obj.data.materials.append(m)
bevel = obj.modifiers.new("Bevel", "BEVEL")
bevel.width, bevel.segments, bevel.limit_method = 0.04, 3, "ANGLE"
for poly in mesh.polygons:
    poly.use_smooth = True

world = bpy.data.worlds.new("World")
world.use_nodes = True
world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.004, 0.006, 0.005, 1)
scene.world = world

def area_light(name, loc, energy, size, color=(1, 1, 1)):
    light = bpy.data.lights.new(name, "AREA")
    light.energy, light.size, light.color = energy, size, color
    o = bpy.data.objects.new(name, light)
    o.location = loc
    o.rotation_euler = (Vector((0, 0, 0)) - Vector(loc)).to_track_quat("-Z", "Y").to_euler()
    scene.collection.objects.link(o)

area_light("Key", (-18, 20, 22), 4000, 14, (0.85, 0.95, 1.0))
area_light("Rim", (18, 26, 8), 2500, 8, (0.5, 1.0, 0.75))
area_light("Fill", (20, -10, 14), 1800, 12, (0.8, 0.9, 1.0))

cam_data = bpy.data.cameras.new("Camera")
cam_data.lens = 42
cam_data.dof.use_dof = True
cam_data.dof.aperture_fstop = 2.2
cam = bpy.data.objects.new("Camera", cam_data)
cam.location = (2, -25, 9)
target = Vector((0, 1, 4))
cam.rotation_euler = (target - cam.location).to_track_quat("-Z", "Y").to_euler()
cam_data.dof.focus_distance = (target - cam.location).length
scene.collection.objects.link(cam)
scene.camera = cam

scene.render.engine = "CYCLES"
scene.render.resolution_x, scene.render.resolution_y = 1600, 1000
scene.render.resolution_percentage = PERCENT
scene.render.image_settings.file_format = "PNG"
scene.cycles.samples = SAMPLES
scene.cycles.use_adaptive_sampling = False
scene.cycles.use_denoising = True
scene.view_settings.view_transform = "AgX"
scene.view_settings.look = "AgX - Medium High Contrast"

bpy.ops.wm.save_as_mainfile(filepath=bpy.path.abspath("//scene.blend") if bpy.data.filepath else "scene.blend")
print(f"Saved scene.blend: {len(mesh.polygons)} faces, {SAMPLES} samples at {PERCENT}%")
```

</details>

## Previewing on the workspace desktop

Every workspace has a Linux desktop ([how we made it fast](/blog/faster-smoother-computer-use)), and Blender's UI runs on it with software OpenGL (Mesa's llvmpipe). That helps when a person wants to orbit the scene or check the framing. Cycles' rendered viewport works too, on the CPU. After a minute it was still grainy:

![Blender 5.2.2 on the workspace desktop with a grainy Cycles viewport of the hexagon scene](/blog/blender-with-ai-agents-workspace-viewport.jpg)

*Blender 5.2.2 on the workspace desktop, rendered viewport after 60 seconds on 8 vCPUs.*

The full frame on the same CPU took 8 minutes 13 seconds.

## Sending the render to a GPU

`replicas gpu` is installed and signed in in every workspace. The agent wrote a short script that installs Blender in the sandbox and renders with OptiX. The `--cycles-device` flag is documented in Blender's [command line arguments](https://docs.blender.org/manual/en/latest/advanced/command_line/arguments.html#cycles-render-options).

```bash
#!/bin/sh
set -e
apt-get update -qq && apt-get install -y -qq curl xz-utils libx11-6 libxrender1 libxfixes3 libxi6 libxkbcommon0 libsm6 libice6 libgl1 libxext6 > /dev/null
[ -x /opt/blender/blender ] || { mkdir -p /opt/blender && curl -sSL https://download.blender.org/release/Blender5.2/blender-5.2.2-linux-x64.tar.xz | tar -xJ --strip-components=1 -C /opt/blender; }
/opt/blender/blender -b scene.blend -o //out/render_ -F PNG -f 1 -- --cycles-device "${DEVICE:-OPTIX}"
```

Then it ran the script on an L40S:

```bash
replicas gpu run --gpu L40S --image ubuntu:24.04 --pull out -- sh render.sh
```

That starts a sandbox, copies the working directory in (`scene.blend` is 260 KB), runs the script, copies `out/` back, and stops the sandbox. The whole run took under a minute:

| Step | Time |
|---|---:|
| Start the sandbox and copy files in | 5.9s |
| Install system libraries with `apt-get` | 16.1s |
| Download and unpack Blender 5.2.2 | 22.0s |
| Render 512 samples | 11.9s |
| Copy the PNG back and stop | 1.9s |
| **Total** | **57.8s** |

*Each step timed inside one run. That run also rendered the frame a second time, which is left out of the total.*

At Modal's [L40S price](https://modal.com/pricing) of $0.000542 per second, that's about 3 cents of GPU time, and two thirds of it went to installing Blender. For a batch of frames, keep one sandbox open with `replicas gpu create` and render many times in it. The second render in the same sandbox took 8.6 seconds.

The CPU and GPU frames came out the same. The SSIM between the two PNGs was 0.99999.

## Which GPU to pick

Next I ran the same file on six GPU types, each in a fresh sandbox. Times are Blender's own render timer, and the cost is that time at Modal's per-second price:

Same .blend, first render on each device. Blender 5.2.2, Cycles, OptiX on GPUs, 1600×1000, 512 samples.

| Device | Render time (seconds) | Kernel loading (seconds) | GPU cost |
| --- | --- | --- | --- |
| Workspace CPU | 493.2 | 0.0 | no GPU |
| L40S | 9.6 | 1.0 | $0.0052 |
| A10G | 21.3 | 1.0 | $0.0065 |
| L4 | 21.4 | 1.3 | $0.0048 |
| T4 | 45.0 | 1.4 | $0.0074 |
| H100 | 276.0 | 261.7 | $0.3028 |
| A100 40GB | 299.3 | 279.6 | $0.1745 |

The most expensive GPUs were the slowest. On the A100 and H100, Blender spent about 4.5 minutes on "Loading render kernels" before the first sample. Blender 5.2.2 ships precompiled CUDA kernels for `sm_50`, `sm_52`, `sm_60`, `sm_70`, `sm_75`, `sm_86`, and `sm_120`. The L4 and L40S (`sm_89`) can run the `sm_86` kernels. The A100 (`sm_80`) and H100 (`sm_90`) can't, so the driver compiles them on first use, and that work is gone when the sandbox stops. Even after compiling, the second H100 render took 14.8s, slower than the L40S at twice the price per second.

In this test, the L40S was the fastest and the L4 the cheapest per frame. CUDA instead of OptiX on the L4 was 18% slower, at 23.8s against 20.1s.

## Other GPU apps work the same way

None of this is specific to Blender. If an app has a CLI or an HTTP API, the agent can install it in a sandbox, run it, and pull the results back.

### Video encoding with NVENC

I transcoded the start of Blender's [Big Buck Bunny 4K master](https://download.blender.org/demo/movies/BBB/) with the same ffmpeg 8.1 build in the workspace and on an L4:

| Job | Workspace CPU, 8 vCPUs | L4 with NVENC |
|---|---|---|
| 60s of 4K to 1080p H.264 | 29.5s · 26.6 MB · SSIM 0.983 | 9.1s · 26.3 MB · SSIM 0.973 |
| 20s of 4K to 4K HEVC | 56.2s · 22.9 MB · SSIM 0.993 | 7.3s · 25.3 MB · SSIM 0.990 |

The CPU side used x264 and x265 at the `medium` preset. NVENC used preset `p5` at about the same bitrates. The GPU was 3× faster for 1080p H.264 and 7.7× faster for 4K HEVC, with slightly lower quality per byte. For one short 1080p clip, the workspace CPU is fine. For 4K or a folder of files, the GPU pays off. The source file is 633 MB, so the agent downloaded it inside the sandbox. Each push or pull is capped at 1 GiB.

### Apps with a UI: ComfyUI through `--port`

Some GPU apps are meant to be used in a browser. `--port` gives a port in the sandbox an HTTPS URL, so the agent can start ComfyUI on an A10G and open it in the workspace browser, or send the link to a person:

```bash
replicas gpu create --gpu A10G --port 8188
replicas gpu push comfy.sh
replicas gpu exec --detach -- sh comfy.sh
```

![ComfyUI running on a cloud A10G, opened in Chrome on the workspace desktop, with a generated hexagon texture](/blog/blender-with-ai-agents-comfyui.jpg)

*ComfyUI's SDXL Turbo template on an A10G. 8.3s for the first 512×512 image, which includes loading the model, then 0.6s.*

Setup took longer than generating. The default image's PyTorch was too old for current ComfyUI, so the agent upgraded it, and the 6.9 GB SDXL Turbo checkpoint took 5m 23s to download inside the sandbox. The URL is public, and ComfyUI didn't ask me to sign in, so anyone with the link can use it until the sandbox stops.

## Other ways to run Blender with an agent

For some work, another setup fits better:

| Setup | Where Blender runs | Good for | Watch out for |
|---|---|---|---|
| [Blender MCP](https://github.com/ahujasid/blender-mcp) | Your computer and GPU | Modeling with Claude, Cursor, Codex, and others while you watch the viewport | It runs arbitrary Python in your Blender session, its socket has no authentication, and your machine has to stay on |
| [SheepIt](https://www.sheepit-renderfarm.com/faq) | Other members' machines | Free renders of finished animations | You earn queue priority by rendering for others, and projects are capped at 2,048 MB |
| [Fox Renderfarm](https://www.foxrenderfarm.com/blender-render-farm.html) | Their render nodes | Paid renders, with a Blender plugin and a $25 trial | Built for uploading a finished `.blend`, not for an agent's edit-and-render loop |
| [Modal](https://modal.com/docs/examples/blender_video), [RunPod](https://www.runpod.io/pricing), [Vast.ai](https://vast.ai/pricing) | GPUs you rent | Lower hourly prices, consumer cards like the RTX 4090, and full control | You manage the machines, images, and credentials, and give the agent access to them |
| Replicas GPU sandboxes | A GPU the agent starts from its workspace | Agent loops that edit, render, and check next to the code | Datacenter GPUs only, plus the limits below |

Renting directly is cheaper per hour: RunPod lists an L40S at $0.79 to $1.09 per hour, while Modal charges $1.95. Modal's own [Blender example](https://modal.com/docs/examples/blender_video) is the one to read if you want to fan frames out across many GPUs yourself. It renders on up to 10 L40S GPUs in parallel.

## Limits to plan around

From the [GPU sandbox docs](https://docs.replicas.dev/features/gpu-sandboxes), plus one thing I noticed:

- 3 running sandboxes per workspace and 10 per organization.
- A 1-hour lifetime by default, up to 24 hours with `--timeout`.
- Sandboxes stop after 10 minutes with no running command, or up to 1 hour with `--idle-timeout`.
- 1 GiB per `push` or `pull`. Download large files inside the sandbox.
- Sandboxes stop when their workspace sleeps, is archived, or is deleted, and their files go with them. Pull results first.
- Not available to organizations with a signed BAA.
- `nproc` reported 1 CPU in the sandboxes I checked, so unpacking Blender was slow. Build scenes in the workspace and send only the render.

GPU sandboxes are in every Replicas workspace. The [docs](https://docs.replicas.dev/features/gpu-sandboxes) cover every flag.

