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.
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), missingStep 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_foreachhas 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:
- bpy.ops.wm covers
open_mainfileandsave_as_mainfile - bpy.utils covers
blend_paths - bpy.types.BlendData covers
orphans_purge - Tips and Tricks, including running scripts from the command line
- Blender Python API index



