RESOLVE API / OUTPUT LAB / 06

Render Settings: exact keys and a reviewable request

Settings are not a queue. True is not a finished export.

Prepare SetRenderSettings with a bounded JSON object: exact keys, strict types, an existing directory and a reviewed plan. No unknown fields, directory creation, hidden codec changes or render start.

THE RESULT

One validated settings request is accepted or rejected. Actual Deliver parameters and the output file are not automatically declared verified.

PYTHON / RESOLVESTUDY MODEL
01settings = {"FormatWidth": 1920, "FormatHeight": 1080}

Width and height form a consistent pair, not ResolutionWidth/Height fields.

CHECK

No type coercion.

Switching steps changes the explanation. Nothing connects to Resolve or runs on your computer.

01 / BEFORE THE CODE

Set the conditions before you run.

01

Environment and interface

External Python 3.9+ and a running Resolve Studio installation. S06 uses the published newer .pyi interface, including GetSettings/SetSettings. Compare your installed document; the older README is not silently treated as the same contract. No universal Free/OS/encoder compatibility is claimed.

02

Exact target

Use current project/timeline IDs, not matching names. The catalogue readers need only an open project; V1 and mutations need a timeline. No project is opened or switched automatically.

03

Separate test copy

Plan first, then --apply with exact IDs, --test-copy and the reviewed --expect-plan. Keep rendering and concurrent edits stopped. A fingerprint is not a lock or complete Deliver snapshot.

Current Studio edition guidance and the installed scripting documentation take precedence over older console or Free-edition tutorials.[1][4][3]

02 / API CONTRACT

Which object. Which method. Which result.

ProjectSetRenderSettings(settings)
Input
Dictionary of supported keys; the example deliberately uses a narrow allowlist.
Returns
Boolean response.
Check
Do not interpret True as verification of all settings or a file.
RenderSettingsFormatWidth / FormatHeight
Input
Two integer pixel dimensions.
Returns
Output request fields.
Check
Input must not silently change project resolution.
RenderSettingsTargetDir / CustomName
Input
Existing absolute directory / basename without a path.
Returns
Requested destination and name for a future output.
Check
Directory existence does not prove permissions or a successful write.
RenderSettingsUseUniqueFilenames / UniqueFilenameStyle
Input
Boolean and integer; this example requires true and 1.
Returns
Request unique naming with a suffix.
Check
Not a substitute for filename and file-policy review.
RenderSettingsVideoQuality
Input
0 is Auto; a positive integer requests a bitrate under the contract.
Returns
Input for codecs that support the setting.
Check
Do not guess units or a universally best value.

Resolve method contracts are documented by Blackmagic; Python and author helpers are labelled separately. The defensive checks are our example design. A non-empty object is not proof of full API compatibility.[16]

03 / UNDERSTAND

Exact keys instead of plausible-looking names

The reviewed RenderSettings uses FormatWidth, FormatHeight, UseUniqueFilenames and VideoQuality. ResolutionWidth, ResolutionHeight, UniqueFilenames, Quality and VideoBitrate are not treated as obvious synonyms. An API contract depends on exact spelling, not similar wording.

Choose Format and VideoCodec through SetCurrentRenderFormatAndCodec and Single clip / Individual clips through the separate mode API. AudioBitrate was not found in the reviewed settings fields; a UI control alone does not establish a same-named JSON field. Check your installed interface for the full version-specific list.[16]

04 / UNDERSTAND

Nine keys are intentionally narrower than the full SDK

The tutorial accepts TargetDir, CustomName, ExportVideo, ExportAudio, UseUniqueFilenames, UniqueFilenameStyle, FormatWidth, FormatHeight and VideoQuality. The first six are mandatory. Dimensions are supplied as a pair or omitted; VideoQuality is optional. Omitted fields remain outside this request rather than becoming verified defaults.

The tutorial targets Single clip video: ExportVideo and UseUniqueFilenames must be true and UniqueFilenameStyle must be 1. ExportAudio is an explicit boolean, but that flag does not verify mix, channels, audible content or codec. Audio-only, In/Out, subtitle and alpha workflows need their own contracts; unknown keys are rejected here.[16]

05 / UNDERSTAND

Validate the directory and JSON before connecting

The entry script reads at most 16 KiB of UTF-8 JSON and rejects duplicate keys, NaN, wrong types and extra fields. It does not eval text or import configuration as Python. A string such as "1920" is not silently converted to an integer.

TargetDir must already be an absolute directory on the local OS. No directory creation, ~ expansion or test write is performed. CustomName is a short portable basename without path separators, reserved device names or forbidden characters. These checks reduce ambiguity but do not verify free space, permissions, mount state at render time or filename collisions.[16][10][8]

06 / UNDERSTAND

Bitrate, dimensions and settings outside the request

The documents distinguish Auto, a positive numeric VideoQuality value and string quality levels for applicable codecs. This JSON tutorial accepts only integers starting at zero. The reviewed description is not sufficient to silently multiply every value by 1000: establish units and their Deliver mapping on your own build and codec.

When dimensions are present, the helper requires an exact match in GetRenderResolutions for the current pair. That is conservative tutorial policy, not a claim that custom dimensions are never supported. Omitted dimensions mean inherited output size is neither read nor verified. Neither option changes project working geometry.[16][3]

07 / UNDERSTAND

Build and validate a local draft

Edit an object or choose an educational sample. The parser distinguishes duplicate keys, unknown fields, wrong types and incomplete dimension pairs. Invalid input hides the previous numerical result and disables copying or downloading an invalid draft.

The .json file contains the current validated object, not the initial sample. The browser cannot inspect your filesystem or Resolve: directory, codec, license, target and native behavior remain unverified. Replace /path/to/existing/output before running Python.[16]

RENDER / CONTRACT LABLOCAL DRAFT

No upload, storage or automatic redaction. Do not enter private paths. Edits disappear when you leave.

Keys and value types match this tutorial.

Output request: 1920 × 1080. Audio: requested.
This is not a picture preview or a connection to Resolve.

Not checked: OS path, directory existence, permissions, codec support, project identity, preset contents or actual file. Replace the sample path yourself.

The nine allowed keys

TargetDirCustomNameExportVideoExportAudioUseUniqueFilenamesUniqueFilenameStyleFormatWidthFormatHeightVideoQuality

This is intentionally narrower than the full SDK. Container/codec and render mode use separate methods; ranges and queue operations belong to later guides.

08 / UNDERSTAND

After True: manual review, not automatic rendering

The plan binds the request to project/timeline IDs, observed pair, mode and a limited settings projection. It does not include all hidden Deliver state. Stop concurrent edits before apply; matching reads are not a transaction and cannot eliminate races.

After one SetRenderSettings call, True plus valid target readback produces accepted_pending_manual_verification and exit code 3. That is not an instruction to repeat the request. Inspect Deliver, then separately create and verify a job. False, an exception or a target change leaves outcome_unconfirmed; there are no automatic retries, rollbacks or fallback parameters.[16]

PYTHON / COMPLETE EXAMPLE

Read the code. Then run it deliberately.

Three Python files; JSON for the settings example

Put set_render_settings.py, resolve_api_output.py and resolve_api_connection.py in one folder. The bundle has six examples, both helpers, audit_output_contract.py, a settings JSON template and RU/EN instructions. The template contains a placeholder path; review it before use. No official SDK files are bundled.

Download all examples + instructions (.zip) ↓
set_render_settings.py
"""Validate a small explicit JSON settings object and send it once. Never render."""
import argparse
from resolve_api_connection import connect
from resolve_api_output import cli_target, cli_write, load_settings, print_result, run_action


def main():
    p = argparse.ArgumentParser(description=__doc__)
    cli_target(p)
    cli_write(p)
    p.add_argument("--settings", required=True, help="Local UTF-8 JSON file, at most 16 KiB")
    a = p.parse_args()

    def task():
        payload = load_settings(a.settings)  # Reject invalid settings before native connection.
        return run_action(connect(), a.project_id, a.timeline_id, "settings", settings=payload,
                          apply=a.apply, expect_plan=a.expect_plan, test_copy=a.test_copy)

    return print_result(task)


if __name__ == "__main__":
    raise SystemExit(main())

Output helper — exact targets and one settings call

Author tutorial code, not an SDK. Plans compare observed state without locking Resolve. Each apply can send only one mutating call; no render jobs, retries, directory creation or automatic recovery. A preset name does not fingerprint preset contents. Accepted settings remain pending manual verification.

resolve_api_output.py
"""S06 author tutorials: output settings, NOT the Blackmagic SDK.

A plan is the default. Each apply sends at most one mutating Resolve call.
No queue creation, rendering, directory creation, retry, rollback or project
save. Observations are not atomic. There is no generic GetRenderSettings in
our reviewed interface: accepted settings/presets still need manual review.
Targets and bounds are tutorial policy, not Resolve product limitations.
"""
from __future__ import annotations
import argparse
import copy
import hashlib
import json
import math
import re
from pathlib import Path

MAX_ITEMS = 5000
MAX_DIMENSION = 32768  # Tutorial bound, not a claim about encoder support.
MAX_JSON_BYTES = 16384
SETTING_KEYS = frozenset({"TargetDir", "CustomName", "ExportVideo", "ExportAudio",
                          "UseUniqueFilenames", "UniqueFilenameStyle", "FormatWidth",
                          "FormatHeight", "VideoQuality"})
OBSERVED_SETTINGS = ("timelineResolutionWidth", "timelineResolutionHeight",
                     "timelinePixelAspectRatio", "timelineFrameRate",
                     "timelineOutputResMatchTimelineRes", "timelineOutputResolutionWidth",
                     "timelineOutputResolutionHeight", "useCustomSettings")


class PreflightError(RuntimeError):
    """Failure before this helper sends any mutation."""


def read(obj, method, *args):
    fn = getattr(obj, method, None)
    if not callable(fn):
        raise PreflightError("Required method unavailable: " + method)
    try:
        return fn(*args)
    except Exception as exc:
        raise PreflightError("Read failed: " + method + " (" + type(exc).__name__ + ")") from exc


def string(value, label):
    if not isinstance(value, str) or not value.strip() or len(value) > 4096:
        raise PreflightError("Missing or invalid " + label)
    try:
        value.encode("utf-8")
    except UnicodeError as exc:
        raise PreflightError("Invalid Unicode in " + label) from exc
    return value


def integer(value, label, low=0, high=MAX_ITEMS):
    if type(value) is not int or not low <= value <= high:
        raise PreflightError(label + " must be an integer in the tutorial's explicit bounds")
    return value


def boolean(value, label):
    if type(value) is not bool:
        raise PreflightError(label + " must be a boolean, not None/0/1/text")
    return value


def finite_json(value, label="API value"):
    """No default=str coercion of unknown native objects."""
    if value is None or type(value) in (str, int, bool):
        return value
    if type(value) is float and math.isfinite(value):
        return value
    if isinstance(value, list):
        return [finite_json(v, label) for v in value]
    if isinstance(value, dict) and all(isinstance(k, str) for k in value):
        return {k: finite_json(v, label) for k, v in value.items()}
    raise PreflightError("Unsupported JSON type in " + label)


def fingerprint(value):
    data = json.dumps(value, sort_keys=True, ensure_ascii=False, allow_nan=False, separators=(",", ":"))
    return hashlib.sha256(data.encode("utf-8")).hexdigest()


def target(resolve, project_id=None, timeline_id=None, need_timeline=True):
    manager = read(resolve, "GetProjectManager")
    if not manager:
        raise PreflightError("Project Manager unavailable")
    project = read(manager, "GetCurrentProject")
    if not project:
        raise PreflightError("No current project")
    pid = string(read(project, "GetUniqueId"), "project ID")
    if project_id is not None and pid != project_id:
        raise PreflightError("Active project does not match --project-id")
    if not need_timeline:
        return project, None, pid, None
    timeline = read(project, "GetCurrentTimeline")
    if not timeline:
        raise PreflightError("No current timeline")
    tid = string(read(timeline, "GetUniqueId"), "timeline ID")
    if timeline_id is not None and tid != timeline_id:
        raise PreflightError("Active timeline does not match --timeline-id")
    return project, timeline, pid, tid


def preset_names(project):
    raw = read(project, "GetRenderPresetList")
    if not isinstance(raw, list) or len(raw) > MAX_ITEMS:
        raise PreflightError("Expected the documented list of preset names; None is not []")
    return [string(x, "preset name") for x in raw]


def list_presets(resolve, project_id=None):
    project, _, pid, _ = target(resolve, project_id, need_timeline=False)
    names = preset_names(project)
    if preset_names(project) != names:
        raise PreflightError("Preset catalogue changed while reading")
    target(resolve, pid, need_timeline=False)
    return {"status": "read_only", "project_id": pid, "presets": names,
            "duplicates": sorted({n for n in names if names.count(n) > 1}),
            "empty": len(names) == 0, "loaded_preset": False,
            "scope": "Names only; no preset content, project preset or Quick Export query"}


def parse_resolution(raw):
    if not isinstance(raw, str) or len(raw) > 100:
        raise PreflightError("Resolution must be an explicit width x height string")
    match = re.fullmatch(r"\s*([0-9]+)\s*[xX×]\s*([0-9]+)\s*", raw)
    if not match:
        raise PreflightError("Unknown Resolution format; no search through unrelated properties")
    return tuple(integer(int(v), "dimension", 1, MAX_DIMENSION) for v in match.groups())


def v1_report(timeline):
    n = integer(read(timeline, "GetTrackCount", "video"), "video track count", 0, 128)
    if n == 0:
        return {"track_exists": False, "track_enabled": None, "items": [],
                "active_sizes": [], "complete": False, "reason": "no_v1"}
    enabled = boolean(read(timeline, "GetIsTrackEnabled", "video", 1), "V1 enabled")
    items = read(timeline, "GetItemListInTrack", "video", 1)
    if not isinstance(items, list) or len(items) > MAX_ITEMS:
        raise PreflightError("V1 list unreadable or beyond tutorial bound")
    rows, ids, active_sizes = [], set(), set()
    active_count, complete = 0, True
    for item in items:
        uid = string(read(item, "GetUniqueId"), "timeline item ID")
        if uid in ids:
            raise PreflightError("Duplicate timeline item ID; target is ambiguous")
        ids.add(uid)
        active = enabled and boolean(read(item, "GetClipEnabled"), "clip enabled")
        row = {"id": uid, "active_on_v1": active, "status": "unread"}
        active_count += int(active)
        try:
            source = read(item, "GetMediaPoolItem")
            if not source:
                row["status"] = "no_media_pool_item"
            else:
                row["source_id"] = string(read(source, "GetUniqueId"), "source ID")
                raw = read(source, "GetClipProperty", "Resolution")
                row["resolution_raw"] = finite_json(raw)
                try:
                    w, h = parse_resolution(raw)
                    row.update(status="dimensions_read", width=w, height=h)
                    if active:
                        active_sizes.add((w, h))
                except PreflightError:
                    row["status"] = "unparsed_resolution"
        except PreflightError as exc:
            row.update(status="read_error", error=str(exc))
        if active and row["status"] != "dimensions_read":
            complete = False
        rows.append(row)
    return {"track_exists": True, "track_enabled": enabled, "items": sorted(rows, key=lambda r: r["id"]),
            "active_count": active_count, "active_sizes": [list(v) for v in sorted(active_sizes)],
            "complete": bool(complete and active_count),
            "scope": "Reported source pixel dimensions only; no rotation, PAR, scaling, visibility or decoding analysis"}


def inspect_v1(resolve, project_id=None, timeline_id=None):
    _, t, pid, tid = target(resolve, project_id, timeline_id)
    first = v1_report(t)
    second = v1_report(t)
    if first != second:
        raise PreflightError("V1 observations changed")
    target(resolve, pid, tid)
    return {"status": "read_only" if second["complete"] else "partial_observation",
            "project_id": pid, "timeline_id": tid, "v1": second}


def mapping(value, label):
    if not isinstance(value, dict) or len(value) > MAX_ITEMS:
        raise PreflightError("Expected dictionary: " + label)
    return {string(k, label + " description"): string(v, label + " ID") for k, v in value.items()}


def format_catalog(project, container="mov"):
    formats = mapping(read(project, "GetRenderFormats"), "render formats")
    if container not in formats.values():
        raise PreflightError("Requested extension is not advertised by GetRenderFormats")
    codecs = mapping(read(project, "GetRenderCodecs", container), "render codecs")
    return {"formats": formats, "container": container, "codecs": codecs}


def current_pair(project):
    pair = mapping(read(project, "GetCurrentRenderFormatAndCodec"), "current pair")
    if not {"format", "codec"} <= pair.keys():
        raise PreflightError("Current format/codec keys are missing")
    return {"format": pair["format"], "codec": pair["codec"]}


def advertised_sizes(project, pair):
    raw = read(project, "GetRenderResolutions", pair["format"], pair["codec"])
    if not isinstance(raw, list) or len(raw) > MAX_ITEMS:
        raise PreflightError("Resolution catalogue is not a list")
    sizes = set()
    for row in raw:
        if not isinstance(row, dict) or not {"Width", "Height"} <= row.keys():
            raise PreflightError("Invalid resolution catalogue row")
        sizes.add((integer(row["Width"], "Width", 1, MAX_DIMENSION),
                   integer(row["Height"], "Height", 1, MAX_DIMENSION)))
    return [list(v) for v in sorted(sizes)]


def observe_settings(obj):
    raw = read(obj, "GetSettings")
    if not isinstance(raw, dict):
        raise PreflightError("GetSettings must return a dictionary; use the interface matching your build")
    return {k: finite_json(raw[k], k) for k in OBSERVED_SETTINGS if k in raw}


def capture(resolve, project_id, timeline_id, with_v1=False):
    project, timeline, pid, tid = target(resolve, project_id, timeline_id)
    jobs = read(project, "GetRenderJobList")
    if not isinstance(jobs, list) or len(jobs) > MAX_ITEMS:
        raise PreflightError("Queue identifiers cannot be observed")
    ids = []
    for job in jobs:
        if not isinstance(job, dict):
            raise PreflightError("Unexpected render job row")
        ids.append(string(job.get("JobId"), "render job ID"))
    if len(ids) != len(set(ids)):
        raise PreflightError("Duplicate queue IDs")
    state = {"project_id": pid, "timeline_id": tid,
             "pair": current_pair(project),
             "mode": integer(read(project, "GetCurrentRenderMode"), "render mode", 0, 1),
             "rendering": boolean(read(project, "IsRenderingInProgress"), "rendering flag"),
             "queue_ids": sorted(ids), "project_settings": observe_settings(project),
             "timeline_settings": observe_settings(timeline)}
    if with_v1:
        state["v1"] = v1_report(timeline)
        state["timeline_count"] = integer(read(project, "GetTimelineCount"), "timeline count", 1, MAX_ITEMS)
    target(resolve, pid, tid)
    return state, project


def stable_capture(resolve, pid, tid, with_v1=False):
    first, _ = capture(resolve, pid, tid, with_v1)
    second, project = capture(resolve, pid, tid, with_v1)
    if first != second:
        raise PreflightError("Observed state changed during preflight")
    return second, project


def no_duplicate_keys(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise PreflightError("Duplicate JSON key: " + key)
        result[key] = value
    return result


def validate_settings(value, check_path=True):
    if not isinstance(value, dict) or not value:
        raise PreflightError("Expected a nonempty flat settings object")
    unknown = set(value) - SETTING_KEYS
    if unknown:
        raise PreflightError("Outside this tutorial's allowlist: " + ", ".join(sorted(unknown)))
    required = {"TargetDir", "CustomName", "ExportVideo", "ExportAudio", "UseUniqueFilenames", "UniqueFilenameStyle"}
    if not required <= value.keys():
        raise PreflightError("Missing required keys: " + ", ".join(sorted(required - value.keys())))
    directory = string(value["TargetDir"], "TargetDir")
    if any(ord(c) < 32 for c in directory):
        raise PreflightError("Control character in TargetDir")
    name = string(value["CustomName"], "CustomName")
    if len(name) > 120 or name != name.strip() or name.endswith(".") or re.search(r'[<>:"/\\|?*\x00-\x1f]', name):
        raise PreflightError("CustomName must be a portable basename, not a path")
    if name in (".", "..") or re.fullmatch(r"(?i)(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(?:\..*)?", name):
        raise PreflightError("Reserved CustomName")
    if value["ExportVideo"] is not True or value["UseUniqueFilenames"] is not True:
        raise PreflightError("This video tutorial requires ExportVideo and UseUniqueFilenames = true")
    boolean(value["ExportAudio"], "ExportAudio")
    integer(value["UniqueFilenameStyle"], "UniqueFilenameStyle", 1, 1)
    if ("FormatWidth" in value) != ("FormatHeight" in value):
        raise PreflightError("Provide both FormatWidth and FormatHeight")
    for key in ("FormatWidth", "FormatHeight"):
        if key in value:
            integer(value[key], key, 1, MAX_DIMENSION)
    if "VideoQuality" in value:
        integer(value["VideoQuality"], "VideoQuality", 0, 1000000000)
    if check_path:
        p = Path(directory)
        if not p.is_absolute() or not p.is_dir():
            raise PreflightError("TargetDir must already be an absolute local directory; no mkdir or ~ expansion")
    return copy.deepcopy(value)


def load_settings(path):
    with Path(path).open("rb") as f:
        raw = f.read(MAX_JSON_BYTES + 1)
    if len(raw) > MAX_JSON_BYTES:
        raise PreflightError("Settings file exceeds 16 KiB")
    try:
        value = json.loads(raw.decode("utf-8"), object_pairs_hook=no_duplicate_keys,
                           parse_constant=lambda x: (_ for _ in ()).throw(PreflightError("Non-finite JSON number")))
    except (ValueError, UnicodeError) as exc:
        raise PreflightError("Invalid UTF-8 JSON settings") from exc
    return validate_settings(value)


def prepare(resolve, pid, tid, action, **options):
    with_v1 = action == "resolution"
    state, project = stable_capture(resolve, pid, tid, with_v1)
    if state["rendering"]:
        raise PreflightError("Rendering is active; do not change Deliver settings")
    plan = {"schema": "resolve.output.plan.v1", "action": action, "observed_before": state,
            "verification_scope": "Selected observations only; no complete render-settings getter or atomic snapshot"}
    if action == "pair":
        codec = string(options.get("codec"), "exact codec ID")
        catalogue = format_catalog(project, "mov")
        labels = [k for k, v in catalogue["codecs"].items() if v == codec]
        if not labels or not all(re.search(r"(?i)(h\.?265|hevc)", k) for k in labels):
            raise PreflightError("Chosen codec must be advertised as H.265/HEVC for mov; no alias probing")
        if state["mode"] != 1:
            raise PreflightError("Select Single clip manually first; this example does not change render mode")
        plan.update(method="SetCurrentRenderFormatAndCodec", args=["mov", codec], catalogue=catalogue,
                    write_required=state["pair"] != {"format": "mov", "codec": codec})
    elif action == "preset":
        name = string(options.get("preset"), "exact preset name")
        names = preset_names(project)
        if names.count(name) != 1:
            raise PreflightError("Exact preset name must occur once; no substring/fallback selection")
        plan.update(method="LoadRenderPreset", args=[name], preset_names=names, write_required=True,
                    preset_content="not observable; same name does not bind preset contents")
    elif action == "settings":
        payload = validate_settings(options.get("settings"))
        if state["mode"] != 1:
            raise PreflightError("Select Single clip manually first")
        if "FormatWidth" in payload:
            sizes = advertised_sizes(project, state["pair"])
            if [payload["FormatWidth"], payload["FormatHeight"]] not in sizes:
                raise PreflightError("Dimensions are not in the advertised list; custom sizes need a separate qualification")
            plan["advertised_sizes"] = sizes
        plan.update(method="SetRenderSettings", args=[payload], write_required=True)
    elif action == "resolution":
        width = integer(options.get("width"), "width", 1, MAX_DIMENSION)
        height = integer(options.get("height"), "height", 1, MAX_DIMENSION)
        scope = options.get("scope")
        if scope not in ("render", "project"):
            raise PreflightError("Choose --scope render or project explicitly")
        v1 = state["v1"]
        if not v1["complete"] or [width, height] not in v1["active_sizes"]:
            raise PreflightError("Need complete active V1 dimensions and an explicitly selected reported pair")
        if len(v1["active_sizes"]) > 1 and options.get("accept_mixed") is not True:
            raise PreflightError("Mixed dimensions: review the report and explicitly --accept-mixed")
        plan["scope"] = scope
        if scope == "render":
            if state["mode"] != 1:
                raise PreflightError("Choose Single clip before setting output dimensions")
            sizes = advertised_sizes(project, state["pair"])
            if [width, height] not in sizes:
                raise PreflightError("Selected dimensions are not advertised for the current pair")
            plan.update(method="SetRenderSettings", args=[{"FormatWidth": width, "FormatHeight": height}],
                        advertised_sizes=sizes, write_required=True)
        else:
            if state["timeline_count"] != 1 or state["timeline_settings"].get("useCustomSettings") != "0":
                raise PreflightError("Project-scope tutorial needs one timeline using project settings; no forced inheritance")
            for current in (state["project_settings"], state["timeline_settings"]):
                for key in ("timelineResolutionWidth", "timelineResolutionHeight"):
                    raw = current.get(key)
                    if not isinstance(raw, str) or not re.fullmatch(r"[0-9]+", raw):
                        raise PreflightError("Project/timeline dimension readback unavailable")
                    integer(int(raw), key, 1, MAX_DIMENSION)
            payload = {"timelineResolutionWidth": str(width), "timelineResolutionHeight": str(height)}
            plan.update(method="SetSettings", args=[payload], write_required=any(
                state[key].get(k) != v for key in ("project_settings", "timeline_settings") for k, v in payload.items()))
    else:
        raise PreflightError("Unknown action")
    if not callable(getattr(project, plan["method"], None)):
        raise PreflightError("Required write method is not exposed; do not guess alternate setters")
    plan["plan_sha256"] = fingerprint(plan)
    return plan, project


def run_action(resolve, project_id, timeline_id, action, *, apply=False, expect_plan=None,
               test_copy=False, preset_reviewed=False, **options):
    plan, _ = prepare(resolve, project_id, timeline_id, action, **options)
    if not apply:
        return {"status": "plan_only", "plan": plan}
    if not test_copy or not project_id or not timeline_id or not expect_plan:
        raise PreflightError("Apply requires exact IDs, --test-copy and --expect-plan")
    if action == "preset" and not preset_reviewed:
        raise PreflightError("Review the broad preset contents and pass --preset-reviewed")
    if expect_plan != plan["plan_sha256"]:
        raise PreflightError("Reviewed plan does not match current observed fields")
    fresh, project = prepare(resolve, project_id, timeline_id, action, **options)
    if fresh != plan:
        raise PreflightError("Plan changed immediately before apply")
    if not plan["write_required"]:
        return {"status": "already_satisfied_observed_scope", "plan": plan, "mutation_sent": False}
    # Exactly one invocation. Any exception below can follow partial side effects.
    report = {"status": "outcome_unconfirmed", "plan": plan, "mutation_sent": True,
              "retry_allowed": False, "full_render_settings_verified": False}
    try:
        response = getattr(project, plan["method"])(*plan["args"])
        report["response_type"] = type(response).__name__
        report["returned_true"] = response is True
    except Exception as exc:
        report["error"] = "Mutation raised " + type(exc).__name__
        response = None
    try:
        after, _ = stable_capture(resolve, project_id, timeline_id, action == "resolution")
        report["observed_after"] = after
    except Exception as exc:
        report["readback_error"] = type(exc).__name__ + ": " + str(exc)
        return report
    before = plan["observed_before"]
    expected = copy.deepcopy(before)
    if action == "pair":
        expected["pair"] = {"format": "mov", "codec": options["codec"]}
    elif action == "resolution" and plan["scope"] == "project":
        for key in ("project_settings", "timeline_settings"):
            expected[key].update(plan["args"][0])
    if action == "preset":
        # Pair/mode may change on purpose; IDs, queue membership and project/timeline
        # geometry must still be the fields observed, not a different target.
        observed_ok = all(after[k] == before[k] for k in before if k not in ("pair", "mode"))
    else:
        observed_ok = after == expected
    report["observed_scope_matches"] = observed_ok
    if response is True and observed_ok:
        report["status"] = ("verified_observed_scope" if action == "pair" or
                            (action == "resolution" and plan["scope"] == "project")
                            else "accepted_pending_manual_verification")
    return report


def inspect_mov(resolve, project_id=None):
    project, _, pid, _ = target(resolve, project_id, need_timeline=False)
    first = format_catalog(project)
    if format_catalog(project) != first:
        raise PreflightError("Format catalogue changed")
    target(resolve, pid, need_timeline=False)
    return {"status": "read_only", "project_id": pid, "catalogue": first,
            "scope": "Advertised IDs, not an encode test or proof of bit depth, profile or hardware encoder"}


def cli_target(parser, timeline=True):
    parser.add_argument("--project-id", help="Exact current project ID; required for apply")
    if timeline:
        parser.add_argument("--timeline-id", help="Exact current timeline ID; required for apply")


def cli_write(parser):
    parser.add_argument("--apply", action="store_true", help="One mutation after plan checks")
    parser.add_argument("--test-copy", action="store_true", help="Confirm a separate test copy")
    parser.add_argument("--expect-plan", help="SHA-256 printed by the reviewed plan")


def print_result(fn):
    try:
        value = fn()
    except Exception as exc:
        print(json.dumps({"status": "preflight_failed", "error": type(exc).__name__ + ": " + str(exc)}, ensure_ascii=False))
        return 2
    print(json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False))
    # 3 is a deliberate pending/unknown/partial result, NOT permission to retry.
    return 3 if value["status"] in ("outcome_unconfirmed", "accepted_pending_manual_verification", "partial_observation") else 0

Optional: inspect the output contract in your local .pyi

This standalone checker parses a bounded interface file without importing it. Its findings describe that document, not a running Resolve instance or an authorisation to write.

audit_output_contract.py
"""Inspect an installed .pyi for S06 declarations WITHOUT importing or executing it.

An interface declaration is not an encoder capability or successful live call.
Missing names are reported as NOT DECLARED HERE, not as absent in all versions.
"""
import argparse
import ast
import hashlib
import json
from pathlib import Path

QUERIES = {
    "Project": ("GetRenderPresetList", "LoadRenderPreset", "GetRenderFormats", "GetRenderCodecs",
                "GetCurrentRenderFormatAndCodec", "SetCurrentRenderFormatAndCodec", "GetCurrentRenderMode",
                "SetCurrentRenderMode", "GetRenderResolutions", "SetRenderSettings", "GetRenderSettings",
                "GetSettings", "SetSettings"),
    "RenderSettings": ("FormatWidth", "FormatHeight", "ResolutionWidth", "ResolutionHeight",
                       "TargetDir", "CustomName", "UseUniqueFilenames", "UniqueFilenameStyle",
                       "VideoQuality", "Format", "VideoCodec", "IndividualClips", "AudioBitrate"),
}


def audit(path):
    with Path(path).open("rb") as f:
        raw = f.read(2 * 1024 * 1024 + 1)
    if len(raw) > 2 * 1024 * 1024:
        raise ValueError("Document exceeds the 2 MiB tutorial bound")
    tree = ast.parse(raw.decode("utf-8-sig"))
    classes = {n.name: n for n in tree.body if isinstance(n, ast.ClassDef)}
    if not all(name in classes for name in QUERIES):
        raise ValueError("Expected Project and RenderSettings classes in this interface document")
    result = {}
    for owner, names in QUERIES.items():
        found = {}
        for node in classes[owner].body:
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                found[node.name] = {"line": node.lineno, "kind": "method", "arguments": ast.unparse(node.args)}
            elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
                found[node.target.id] = {"line": node.lineno, "kind": "field", "type": ast.unparse(node.annotation)}
        result[owner] = {name: found.get(name, {"status": "not_declared_here"}) for name in names}
    return {"document_sha256": hashlib.sha256(raw).hexdigest(), "declarations": result,
            "live_verified": False, "scope": "This file only; not native execution or encoder support"}


def main():
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument("interface_file", type=Path)
    a = p.parse_args()
    try:
        print(json.dumps(audit(a.interface_file), ensure_ascii=False, indent=2))
    except (OSError, UnicodeError, ValueError, SyntaxError, RecursionError) as exc:
        print(json.dumps({"status": "audit_failed", "error": type(exc).__name__ + ": " + str(exc)}))
        return 2
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Shared connection helper — inspect and download

This file only locates and loads the installed bridge. Native imports can write Python bytecode caches and can block; this is not a sandbox or a timeout mechanism. Use a trusted local installation.

resolve_api_connection.py
"""Load the installed Blackmagic module; do not install a similarly named package.

This helper uses standard desktop installation paths or RESOLVE_SCRIPT_API.
It does not enable scripting, start Resolve, open projects or retry API calls.
Validate Python/native-library compatibility against your installed README.txt.
"""
from __future__ import annotations

import importlib.util
import os
from pathlib import Path
import struct
import sys


class ResolveConnectionError(RuntimeError):
    """An explicit environment, module, or application-connection failure."""


def scripting_root() -> Path:
    configured = os.environ.get("RESOLVE_SCRIPT_API")
    if configured is not None:
        if not configured.strip() or configured != configured.strip():
            raise ResolveConnectionError("RESOLVE_SCRIPT_API is empty or has outer whitespace")
        if configured.startswith(('"', "'")) or configured.endswith(('"', "'")):
            raise ResolveConnectionError("Do not store literal outer quotes in RESOLVE_SCRIPT_API")
        root = Path(os.path.expandvars(configured)).expanduser()
    elif sys.platform == "win32":
        root = Path(os.environ.get("PROGRAMDATA", r"C:\ProgramData")) / "Blackmagic Design/DaVinci Resolve/Support/Developer/Scripting"
    elif sys.platform == "darwin":
        root = Path("/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting")
    elif sys.platform.startswith("linux"):
        root = Path("/opt/resolve/Developer/Scripting")
    else:
        raise ResolveConnectionError("Set RESOLVE_SCRIPT_API for this platform/installation")
    if not root.is_absolute():
        raise ResolveConnectionError("RESOLVE_SCRIPT_API must be an absolute Scripting directory")
    return root


def module_file() -> Path:
    # RESOLVE_SCRIPT_API points to Scripting, not its Modules subdirectory.
    return scripting_root() / "Modules" / "DaVinciResolveScript.py"


def load_resolve_module():
    if struct.calcsize("P") * 8 != 64:
        raise ResolveConnectionError("Use a compatible 64-bit Python interpreter")
    source = module_file()
    if not source.is_file():
        raise ResolveConnectionError("Official DaVinciResolveScript.py not found at: " + str(source))
    configured_lib = os.environ.get("RESOLVE_SCRIPT_LIB")
    if configured_lib is not None:
        lib = Path(os.path.expandvars(configured_lib)).expanduser()
        if not lib.is_absolute() or not lib.is_file():
            raise ResolveConnectionError("RESOLVE_SCRIPT_LIB must name the installed native library file")
    name = "DaVinciResolveScript"
    spec = importlib.util.spec_from_file_location(name, source)
    if spec is None or spec.loader is None:
        raise ResolveConnectionError("Cannot create a loader for the official module")
    previous = sys.modules.get(name)
    module = importlib.util.module_from_spec(spec)
    sys.modules[name] = module
    try:
        spec.loader.exec_module(module)
        # Blackmagic's wrapper can replace its sys.modules entry with a native module.
        loaded = sys.modules[name]
        if not callable(getattr(loaded, "scriptapp", None)):
            raise ResolveConnectionError("Loaded module does not expose scriptapp")
        return loaded
    except Exception as exc:
        if previous is None:
            sys.modules.pop(name, None)
        else:
            sys.modules[name] = previous
        raise ResolveConnectionError("Official module was found, but loading it failed") from exc


def connect():
    module = load_resolve_module()
    try:
        resolve = module.scriptapp("Resolve")
    except Exception as exc:
        raise ResolveConnectionError("scriptapp failed; inspect the original exception") from exc
    if not resolve:
        raise ResolveConnectionError("No Resolve object: check running Studio, Local access and Python environment")
    return resolve


def project_manager(resolve):
    manager = resolve.GetProjectManager()
    if not manager:
        raise ResolveConnectionError("Resolve connected, but Project Manager is unavailable")
    return manager

Launch commands

Plan from JSON

Plan from JSON
python set_render_settings.py --settings settings.example.json --project-id "PROJECT_ID" --timeline-id "TIMELINE_ID"

Replace the sample path first. Create the directory yourself before running.

Apply one object

Apply one object
python set_render_settings.py --settings settings.example.json --project-id "PROJECT_ID" --timeline-id "TIMELINE_ID" --apply --test-copy --expect-plan "PLAN_SHA256"

Editing the file requires a new reviewed plan. True still leaves manual review pending and exits with 3.

Inspect a local interface file

Inspect a local interface file
python audit_output_contract.py "/path/to/DaVinciResolveScript.pyi"

Parses text only. The file is never imported or executed.

What the code does

  1. Connect deliberately

    The shared connection helper loads the official installed bridge. It does not start Resolve, install an SDK or choose a different project.

  2. Read exact fields

    Check documented shapes and preserve uncertainty. Tutorial limits are explicit: 5000 items/catalogue rows, 128 video tracks and 32768 per dimension.

  3. Separate observation from mutation

    The plan binds only the observed projection. One apply sends at most one mutation; there is no retry, fallback setter, project save or queue operation.

  4. Interpret the result

    Exit 0 reports a completed observation, plan or checked narrow scope. Exit 2 is preflight failure. Exit 3 marks partial, unknown or manually unverified state; never retry it blindly.

Boundaries to keep

Author tutorial code, not the Blackmagic SDK. Source review and offline tests do not certify native calls.

No AddRenderJob, StartRendering, queue deletion, automatic directory creation or project save occurs in S06.

The installed interface takes precedence. SetSettings/GetSettings belong to the reviewed newer contract; no older-method fallback is guessed.

Reports may include local paths supplied by you or observed IDs. Redact them manually before sharing; no automatic redaction is promised.

DEBUG / OBSERVATION FIRST

An error is a starting point, not a diagnosis.

Preflight failed

Possible reasonA target, catalogue shape or required setting does not match the reviewed contract.

Next checkInspect the report and your installed documentation. Do not add guessed aliases or replace None with a convenient default.

Partial observation

Possible reasonAn active source, property or catalogue could not be read fully.

Next checkKeep missing entries visible and investigate them. Do not silently skip them to apply a size.

accepted_pending_manual_verification

Possible reasonA setter accepted the request, but full current Render Settings cannot be read with this contract.

Next checkInspect Deliver manually. Do not claim a checked file or use exit 3 as an automatic retry signal.

outcome_unconfirmed

Possible reasonA mutation returned false, raised or could not be verified on the same target.

Next checkEstablish actual application state before any further mutation. No automated rollback or second attempt is performed.

PRACTICE / YOUR OWN EVIDENCE

Check this task on a separate copy

  1. Download the bundle and replace TargetDir in settings.example.json with an existing test directory.
  2. Request a plan with exact IDs and inspect every supplied key.
  3. After one apply, manually inspect Deliver and confirm that the queue has not grown.
ACCEPT WHEN

Exactly the reviewed object is sent, invalid JSON does not initiate a connection, and an accepted request is not called a finished export.

This is a checklist for your test, not a report of a test already performed on the website.

FAQ

Questions that matter in practice.

Where is the full parameter list?

Use the Developer/Scripting documentation shipped with your installation. The included audit_output_contract.py parses a .pyi without execution and reports declarations, not live capabilities.[16]

Why does the JSON validator not accept every SDK field?

The tutorial tool is limited to nine explicitly checked fields. Other fields need additional semantics and dependency checks; they are not declared unavailable in Resolve.[16]

Does exit 3 mean I should retry apply?

No. It means pending manual verification, a partial observation or an unknown outcome. Establish actual state first; do not attach an automatic retry to every nonzero exit.[16]

Can these settings guarantee that an existing file is never overwritten?

One setting is not a universal guarantee. This example does not render. It requests unique naming, but destination, final filename, existing files and write policy require execution-stage checks.[16]

SOURCES / VERSION BOUNDARIES

Sources, not implied certification.

The source review, offline Python tests and UI checks are different kinds of evidence. None establishes that every example works with every Studio build, operating system or Python version. The date below is the editorial review date, not a live-test certificate.

  1. Blackmagic Design — DaVinci Resolve Studio 21.1 release notes

    8 September 2026. The release announcement assigns Python scripting to Studio. Edition requirements, not a test report for our examples.

  2. Blackmagic Design — Support Center

    Release catalogue checked on 5 October 2026: 21.1.1 is listed. The latest release is not the minimum version of every method.

  3. Blackmagic Scripting README — snapshot 24 July 2026 (Resolve 21.0.4)

    Public mirror of Blackmagic’s original README, maintained by X-Raym. Used for basic signatures; not official hosting and not a 21.1.1 README. Locate your installed Developer/Scripting/README.txt through Resolve’s Help/Documentation menu; exact menu nesting can vary.

  4. Blackmagic Design — Scripting and Automation

    Official description of Studio Python/Lua scripting and integrations. Do not extend Studio capabilities to Free without checking the exact release.

  5. Python — pathlib

    Directory enumeration, absolute paths and file probes. A matching extension is not codec support or successful decoding.

  6. Python — json encoder and decoder

    JSON types, ensure_ascii, allow_nan and explicit serialization. Our export never uses default=str to conceal unknown API objects.

  7. Blackmagic DaVinciResolveScript.pyi — output contracts, reviewed 6 October 2026

    Third-party published mirror of Blackmagic’s interface. S06 reviews RenderSettings, preset, format, codec, size methods and GetSettings/SetSettings. This establishes neither a minimum version nor live compatibility. Compare your installed .pyi; the reviewed document does not declare a complete GetRenderSettings.

  8. Blackmagic Design — official release catalogue, checked 6 October 2026

    The official catalogue lists Resolve/Studio 21.1.1 on 6 October 2026. A listed release does not certify these tutorial examples on that build.

KEEP BUILDING

Continue with a precise next task.

API / Render

How to load one exact render preset

Explicitly load one preset with LoadRenderPreset: exact name, current catalogue, a plan and separate Deliver review. No substring selection, alternative probing or full-verification claim from one True.

API / Render

MOV and H.265: select actual API identifiers

Discover available Resolve formats and codecs, choose an exact H.265 ID for MOV and read the pair back. No alias probing, hidden mode changes or automatic Render Job creation.

API / Render

Dimensions from V1: project or render

Choose an explicit size from the V1 report and apply it either to output or project settings. No automatic maximum, partial-success shortcut or simultaneous changes to both scopes.

Back to the API library →MCP and AI assistants →