Simstim

Blender

Batch-process a folder of .blend files from the command line with bpy

A 59-line headless bpy script that audits, fixes and re-saves every .blend file in a folder, with a proper exit code for your farm or CI.

· 4 min read · blender · python · tutorial · bpy · pipeline · automation

Sooner or later someone asks you to change a setting in every shot file, or to find out which files point at textures that no longer exist. Opening them one by one in the UI is slow and easy to get wrong. Blender runs happily without a window, so one script can do the whole folder.

The script below opens every .blend file in a folder, reports object counts and missing files, sets the render resolution, purges orphan data, and saves a copy to an output folder. A file that won't open is logged and skipped, and the exit code tells the caller whether anything failed.

It was tested in Blender 5.2.0 LTS (hash fbe6228777e7, built 2026-07-14) on Windows, run headless with --factory-startup.

Step 1: read arguments after the double dash

Blender treats everything before -- as its own arguments and ignores everything after it, so that is where the script's options go. Slicing sys.argv at -- lets you use plain argparse.

# batch_blend.py: audit and normalise every .blend file in a folder.
# blender -b --factory-startup --python batch_blend.py -- IN_DIR OUT_DIR
import argparse
import sys
from pathlib import Path

import bpy


def parse_args():
    # Blender ignores everything after "--", so our arguments live there.
    argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
    parser = argparse.ArgumentParser(prog="batch_blend")
    parser.add_argument("in_dir", type=Path)
    parser.add_argument("out_dir", type=Path)
    parser.add_argument("--res", type=int, nargs=2, default=[1920, 1080])
    return parser.parse_args(argv)

Step 2: audit the open file

bpy.utils.blend_paths(absolute=True) returns every external file path the open file references, such as images and caches, already resolved from // relative paths. Checking each one with Path.exists() gives you a missing-files report.

def audit():
    scene = bpy.context.scene
    meshes = [ob for ob in scene.objects if ob.type == 'MESH']
    missing = [p for p in bpy.utils.blend_paths(absolute=True)
               if not Path(p).exists()]
    return len(scene.objects), len(meshes), missing

Step 3: apply the fix

This is the part you change for your own job. Here it sets the render resolution to 100% and purges orphans. bpy.data.orphans_purge() removes data-blocks with no users and returns how many it removed. With do_recursive=True it also removes data that becomes orphaned once its only user is gone.

def normalise(res):
    render = bpy.context.scene.render
    render.resolution_x, render.resolution_y = res
    render.resolution_percentage = 100
    return bpy.data.orphans_purge(do_recursive=True)

Step 4: loop, save a copy, report

bpy.ops.wm.open_mainfile replaces the current file, and the script keeps running afterwards. load_ui=False skips the stored UI layout, which you don't need in background mode. If the file can't be read, the operator raises RuntimeError, so one bad file doesn't stop the batch.

save_as_mainfile(..., copy=True) writes to the new path without turning it into the current file. The originals are never overwritten. At the end, sys.exit(1) gives a non-zero exit code if any file failed, which is what a farm job or CI step checks.

def main():
    args = parse_args()
    args.out_dir.mkdir(parents=True, exist_ok=True)
    failed = []
    for path in sorted(args.in_dir.glob("*.blend")):
        try:
            bpy.ops.wm.open_mainfile(filepath=str(path), load_ui=False)
        except RuntimeError as err:
            print(f"FAIL {path.name}: {err}")
            failed.append(path.name)
            continue
        n_obj, n_mesh, missing = audit()
        purged = normalise(args.res)
        out = args.out_dir / path.name
        bpy.ops.wm.save_as_mainfile(filepath=str(out), copy=True)
        print(f"OK   {path.name}: {n_obj} objects, {n_mesh} meshes, "
              f"{purged} orphans purged, {len(missing)} missing files")
        for p in missing:
            print(f"     missing: {p}")
    print(f"DONE {len(failed)} failed: {failed}")
    if failed:
        sys.exit(1)


main()

Put the four blocks together in order and you have the complete 59-line batch_blend.py.

Running it

blender -b --factory-startup --python-exit-code 1 --python batch_blend.py -- in out --res 2048 858

-b runs in the background, and --factory-startup skips your personal startup.blend, so a local setup doesn't change the result. --python-exit-code 1 makes an uncaught Python exception exit with code 1. Without it, a crash in your script can still end with exit code 0.

For the test, the folder held two shot files, each with a material left behind by a deleted mesh and an image pointing at a texture that doesn't exist, plus a text file renamed to broken.blend. This was the output, with the long temp folder shortened to ...:

FAIL broken.blend: Error: File format is not supported in file "...\in\broken.blend"
OK   shot_00.blend: 2 objects, 2 meshes, 1 orphans purged, 1 missing files
     missing: ...\in\textures\missing.png
OK   shot_01.blend: 5 objects, 5 meshes, 1 orphans purged, 1 missing files
     missing: ...\in\textures\missing.png
DONE 1 failed: ['broken.blend']

Blender exited with code 1. A run over only the good files exited with 0. Reopening the saved copies confirmed a resolution of 2048 x 858 at 100%, the same object counts, and no leftover materials.

Going further

  • Run folders in parallel by starting several Blender processes, one per subfolder. Each process is independent.
  • Write the report as JSON instead of printing it, so a pipeline tool can read it.
  • On Blender 5.2, bpy.data.file_path_foreach has new options to expand UDIM tiles, image sequences and cache files, according to the 5.2 Python API release notes. It wasn't tested for this post, so check the docs before relying on it.

Official API documentation: