01 / BEFORE THE CODE
Set the conditions before you run.
Environment
External Python 3.9+ and a running Resolve Studio with local scripting. Check the interface file supplied with your installed version. This tutorial reviews a published Blackmagic interface mirror on 6 October 2026; it does not claim live certification, minimum-version coverage or Free compatibility.
Exact target
Keep an isolated test project and one operator. Project ID and job ID are exact opaque strings, not a queue position, timeline name or file name. Only adding a new job requires the current timeline; reading and managing existing jobs do not.
Before any write
Read the plan first. Apply requires an exact target, a matching plan hash and explicit test-copy acknowledgement. For add/start, inspect Deliver and the selected queued job yourself. A hash is not a lock, authority check or complete settings 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.
DeleteRenderJob(job_id)- Input
- One nonempty string ID.
- Returns
- Bool.
- Check
- One attempt; never substitute the whole entry object.
GetRenderJobList()- Input
- Before planning and after the call.
- Returns
- RenderJobInfo list.
- Check
- Only the target disappears; observed neighbours remain.
IsRenderingInProgress()- Input
- Before mutation.
- Returns
- Bool.
- Check
- Active processing causes refusal, not automatic stop.
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.[18]
03 / UNDERSTAND
Deletion requires precise addressing
The former example iterated over all entries and tried different argument types, even though the topic promised single-job deletion. The replacement requires exactly one --job-id, locates it in the expected project and does not select by name or row number.
The reviewed DeleteRenderJob contract accepts a string jobId. Keep it unchanged: no numeric coercion, whitespace trimming or lowercasing. A duplicated ID in the queue response is ambiguous and causes refusal rather than selection of the first object.[18]
04 / UNDERSTAND
Deletion must not silently stop rendering
Before mutation, the script requires an idle, understood local queue. It does not call StopRendering to make deletion convenient, and refuses unknown, remote or background states. A Ready entry may be deleted only by an explicit single-target decision, not automatic cleanup.
A missing ID before the call causes refusal, not a fictitious successful-deletion message. Another actor or the wrong project may explain the absence. Establish that separately; another speculative command does not provide better evidence.[18]
05 / UNDERSTAND
Check more than target disappearance
The plan retains observed queue structure. After one DeleteRenderJob, the example requires True and a new list with only the selected ID absent, while other entries retain their order, states and readable fields. This is stricter than observing a count decrease of one.
A mismatch between the return and the queue keeps mutation_outcome_unknown. For example, False with an already absent entry does not permit retry. The helper records the attempt but never tries another argument type or recreates the entry.[18]
06 / UNDERSTAND
What this exercise actually verifies
The check concerns queue state, not the integrity of the entire project. It does not save the project, remove source media, clear an output directory or restore an exported file. Matching observations do not prevent another user from changing the queue later.
After uncertainty, read the project and queue again, retain attempted_job_id and compare with the interface. A separate selected-cleanup workflow handles several reviewed terminal entries and likewise reports partial completion rather than pretending to be atomic.[18]
PYTHON / COMPLETE EXAMPLE
Read the code. Then run it deliberately.
Put delete_render_job.py, resolve_api_queue.py and resolve_api_connection.py in one folder. The bundle contains eight entry scripts, both helpers and RU/EN instructions. No official SDK is included; the installed Blackmagic bridge is used.
Download all examples + instructions (.zip) ↓"""Delete exactly one selected queue item; never stop or purge the queue."""
import argparse
from resolve_api_connection import connect
from resolve_api_queue import cli_write, write_from_args, print_result
def main():
parser = argparse.ArgumentParser(description=__doc__)
cli_write(parser, "delete")
args = parser.parse_args()
return print_result(lambda: write_from_args(connect(), "delete", args))
if __name__ == "__main__":
raise SystemExit(main())
Queue helper — exact IDs, bounded observation and explicit mutations
Author tutorial code, not the SDK. No whole-queue start or purge. StopRendering remains broad. Selected cleanup is sequential and may partially complete; unknown outcomes are never retried. A soft deadline cannot interrupt a blocked native call.
"""S07 author tutorial helpers, NOT the Blackmagic SDK.
Plan-only by default. Opaque IDs; no all-queue start, preset changes, directory
creation, project save, auto retry or rollback. StopRendering is project-wide.
Selected cleanup is deliberately sequential and may partially complete.
Matching observations are not a lock or an atomic snapshot. Poll deadlines
cannot interrupt a blocked native API call. Use an idle, local test project
with one operator; remote/background workflows are outside mutation scope.
"""
from __future__ import annotations
import argparse
import copy
import hashlib
import json
import math
import time
MAX_JOBS = 1000 # Tutorial bounds, not Resolve limits.
MAX_DELETE = 25
MAX_POLLS = 10000
LOCAL_STATES = frozenset({'Ready', 'Rendering', 'Complete', 'Cancelled', 'Failed'})
CANCELLED = frozenset({'Cancelled', 'Background Render Cancelled', 'Remote Render Cancelled'})
EXTERNAL = frozenset({'Ready for background render', 'Ready to remotely render'})
VOLATILE = frozenset({'JobStatus', 'CompletionPercentage', 'TimeTakenToRenderInMs',
'EstimatedTimeRemainingInMs', 'Error'})
class PreflightError(RuntimeError):
"""No mutation has been sent by the failing preflight step."""
def read(obj, method, *args):
fn = getattr(obj, method, None)
if not callable(fn):
raise PreflightError('Missing method: ' + method)
try:
return fn(*args)
except Exception as exc:
raise PreflightError('Read failed: ' + method + ' (' + type(exc).__name__ + ')') from exc
def text(value, name):
if not isinstance(value, str) or not value.strip() or len(value) > 4096:
raise PreflightError('Invalid ' + name + '; expected an exact nonempty string')
if any(ord(c) < 32 for c in value):
raise PreflightError('Control characters in ' + name)
try:
value.encode('utf-8')
except UnicodeError as exc:
raise PreflightError('Invalid Unicode in ' + name) from exc
return value # No trim, lower, int coercion or guessed aliases.
def boolean(value, name):
if type(value) is not bool:
raise PreflightError(name + ' must be bool; None is not False')
return value
def json_value(value, depth=0, budget=None):
if budget is None:
budget = [200000]
budget[0] -= 1
if depth > 8 or budget[0] < 0:
raise PreflightError('API result exceeds tutorial depth/size bounds')
if value is None or type(value) in (bool, int):
return value
if isinstance(value, str) and len(value) <= 32768:
try:
value.encode('utf-8')
except UnicodeError as exc:
raise PreflightError('Invalid API Unicode') from exc
return value
if type(value) is float and math.isfinite(value):
return value
if isinstance(value, list):
return [json_value(v, depth + 1, budget) for v in value]
if isinstance(value, dict) and all(isinstance(k, str) for k in value):
return {k: json_value(v, depth + 1, budget) for k, v in value.items()}
raise PreflightError('Unsupported API JSON value; no default=str conversion')
def digest(value):
data = json.dumps(value, ensure_ascii=False, sort_keys=True, allow_nan=False,
separators=(',', ':')).encode('utf-8')
return hashlib.sha256(data).hexdigest()
def classify_status(raw):
"""Preserve unknown status. Progress/global idle alone never means success."""
if not isinstance(raw, dict):
raise PreflightError('GetRenderJobStatus must return a dict, not None')
raw = json_value(raw)
name = raw.get('JobStatus')
progress = raw.get('CompletionPercentage')
issues = []
if not isinstance(name, str) or not name.strip():
issues.append('missing_or_invalid_JobStatus')
if progress is not None and (type(progress) not in (int, float)
or not math.isfinite(progress) or not 0 <= progress <= 100):
issues.append('invalid_CompletionPercentage')
category = {'Ready': 'ready', 'Rendering': 'rendering', 'Complete': 'complete',
'Failed': 'failed'}.get(name, 'unknown') if isinstance(name, str) else 'unknown'
if isinstance(name, str) and name in CANCELLED:
category = 'cancelled'
if isinstance(name, str) and name in EXTERNAL:
category = 'external'
if category == 'complete' and progress is not None and progress != 100:
issues.append('complete_progress_conflict')
if category == 'complete' and raw.get('Error') not in (None, ''):
issues.append('complete_error_conflict')
if issues:
category = 'unknown'
return {'category': category, 'job_status': name, 'completion_percentage': progress,
'issues': issues, 'raw': raw, 'file_verified': False}
def target(resolve, project_id=None, timeline_id=None, need_timeline=False):
manager = read(resolve, 'GetProjectManager')
if not manager:
raise PreflightError('No Project Manager')
project = read(manager, 'GetCurrentProject')
if not project:
raise PreflightError('No current project')
pid = text(read(project, 'GetUniqueId'), 'project ID')
if project_id is not None and pid != text(project_id, 'expected project ID'):
raise PreflightError('Active project changed or differs from --project-id')
tid = None
if need_timeline:
timeline = read(project, 'GetCurrentTimeline')
if not timeline:
raise PreflightError('No current timeline for AddRenderJob')
tid = text(read(timeline, 'GetUniqueId'), 'timeline ID')
if timeline_id is not None and tid != text(timeline_id, 'expected timeline ID'):
raise PreflightError('Active timeline differs from --timeline-id')
return project, pid, tid
def jobs(project):
raw = read(project, 'GetRenderJobList')
if not isinstance(raw, list) or len(raw) > MAX_JOBS:
raise PreflightError('Expected bounded list from GetRenderJobList; None is not []')
result = []
seen = set()
for row in raw:
if not isinstance(row, dict):
raise PreflightError('RenderJobInfo must be a dict')
item = json_value(row)
jid = text(item.get('JobId'), 'JobId')
if jid in seen:
raise PreflightError('Duplicate JobId; cannot address a unique queue item')
seen.add(jid)
result.append(item)
return result
def queue_structure(rows):
return [{k: v for k, v in r.items() if k not in VOLATILE} for r in rows]
def snapshot(resolve, project_id=None, timeline_id=None, need_timeline=False):
"""Two queue reads bracket status reads; this is still non-atomic."""
project, pid, tid = target(resolve, project_id, timeline_id, need_timeline)
rows = jobs(project)
active = boolean(read(project, 'IsRenderingInProgress'), 'IsRenderingInProgress')
states = {r['JobId']: classify_status(read(project, 'GetRenderJobStatus', r['JobId'])) for r in rows}
if queue_structure(jobs(project)) != queue_structure(rows):
raise PreflightError('Queue structure changed during observation')
if boolean(read(project, 'IsRenderingInProgress'), 'IsRenderingInProgress') != active:
raise PreflightError('Rendering activity changed during observation')
target(resolve, pid, tid, need_timeline)
return project, {'project_id': pid, 'timeline_id': tid, 'is_rendering': active,
'jobs': rows, 'statuses': states}
def stable(snapshot_value):
return {'project_id': snapshot_value['project_id'], 'timeline_id': snapshot_value['timeline_id'],
'is_rendering': snapshot_value['is_rendering'],
'jobs': queue_structure(snapshot_value['jobs']),
'statuses': {jid: {'job_status': v['job_status'], 'category': v['category'],
'issues': v['issues']} for jid, v in snapshot_value['statuses'].items()}}
def public_snapshot(s, details=False):
out = {'project_id': s['project_id'], 'timeline_id': s['timeline_id'],
'is_rendering': s['is_rendering'], 'count': len(s['jobs']),
'job_ids': [r['JobId'] for r in s['jobs']], 'observation_hash': digest(stable(s)),
'statuses': {k: {a: b for a, b in v.items() if a != 'raw'} for k, v in s['statuses'].items()},
'atomic': False, 'file_verified': False}
if details:
out['raw_jobs'] = copy.deepcopy(s['jobs'])
out['raw_statuses'] = {k: copy.deepcopy(v['raw']) for k, v in s['statuses'].items()}
return out
def inspect_queue(resolve, project_id=None, details=False):
_, s = snapshot(resolve, project_id)
return {'status': 'observed', **public_snapshot(s, details)}
def status_once(resolve, project_id, job_id, details=False):
jid = text(job_id, 'job ID')
project, pid, _ = target(resolve, project_id)
rows = jobs(project)
matches = [r for r in rows if r['JobId'] == jid]
if not matches:
return {'status': 'not_found', 'project_id': pid, 'job_id': jid,
'file_verified': False, 'repeat_mutation': False}
state = classify_status(read(project, 'GetRenderJobStatus', jid))
active = boolean(read(project, 'IsRenderingInProgress'), 'IsRenderingInProgress')
after = [r for r in jobs(project) if r['JobId'] == jid]
target(resolve, pid)
if queue_structure(after) != queue_structure(matches):
raise PreflightError('Selected job changed or disappeared during status read')
result = {'status': 'observed', 'project_id': pid, 'job_id': jid,
'is_rendering': active, **{k: v for k, v in state.items() if k != 'raw'}}
if details:
result['raw_status'] = state['raw']
result['raw_job'] = matches[0]
return result
def bounds(timeout, interval, max_polls):
if type(timeout) not in (int, float) or not math.isfinite(timeout) or not 0 < timeout <= 86400:
raise PreflightError('timeout must be finite, > 0 and <= 86400 seconds')
if type(interval) not in (int, float) or not math.isfinite(interval) or not 0.1 <= interval <= 60:
raise PreflightError('interval must be finite and 0.1..60 seconds')
if type(max_polls) is not int or not 1 <= max_polls <= MAX_POLLS:
raise PreflightError('max_polls must be an integer in 1..10000')
def wait_job(resolve, project_id, job_id, timeout=120, interval=1, max_polls=600,
details=False, clock=time.monotonic, sleep=time.sleep):
"""Read-only soft deadline, including Ready. It never stops/restarts Resolve."""
text(project_id, 'project ID')
text(job_id, 'job ID')
bounds(timeout, interval, max_polls)
begin = clock()
deadline = begin + timeout
last = None
samples = 0
def result(outcome):
return {'status': outcome, 'project_id': project_id, 'job_id': job_id,
'samples': samples, 'elapsed_seconds': max(0, clock() - begin),
'last_observation': last, 'file_verified': False,
'render_stopped_by_script': False, 'repeat_mutation': False,
'timeout_kind': 'soft; cannot interrupt a blocked native call'}
try:
while samples < max_polls:
if clock() >= deadline:
return result('timeout_unknown')
last = status_once(resolve, project_id, job_id, details)
samples += 1
# A native call may have returned after the deadline; retain evidence,
# but do not claim a within-budget successful wait.
if clock() >= deadline:
return result('timeout_unknown')
if last['status'] == 'not_found':
return result('job_missing_unknown')
category = last['category']
if category == 'complete':
return result('job_complete_file_unverified')
if category in ('failed', 'cancelled'):
return result('job_' + category)
if category in ('unknown', 'external'):
return result('state_unknown' if category == 'unknown' else 'external_mode_not_supported')
sleep(min(interval, max(0, deadline - clock())))
except KeyboardInterrupt:
return result('observer_interrupted_render_unchanged')
except Exception as exc:
out = result('observation_failed_unknown')
out['error_type'] = type(exc).__name__
return out
return result('poll_limit_unknown')
def selected_ids(ids, allow_empty=False):
if not isinstance(ids, list) or len(ids) > MAX_DELETE or (not ids and not allow_empty):
raise PreflightError('Supply 1..25 explicit job IDs; no wildcard or implicit all')
values = [text(x, 'job ID') for x in ids]
if len(set(values)) != len(values):
raise PreflightError('Duplicate selected job IDs')
return values
def local_scope(s, allow_running=False):
for state in s['statuses'].values():
if not isinstance(state['job_status'], str) or state['job_status'] not in LOCAL_STATES or state['issues']:
raise PreflightError('Unknown or background/remote state: mutation refused')
rendering = [k for k, v in s['statuses'].items() if v['category'] == 'rendering']
if not allow_running and (s['is_rendering'] or rendering):
raise PreflightError('Queue must be idle; this script will not stop it first')
if allow_running and bool(rendering) != s['is_rendering']:
raise PreflightError('Global rendering flag and job states disagree')
return rendering
def plan_action(resolve, action, project_id=None, job_ids=None, timeline_id=None):
if action not in ('add', 'start', 'delete', 'cleanup', 'stop'):
raise PreflightError('Unknown action')
ids = selected_ids(job_ids or [], allow_empty=action in ('add', 'stop'))
if action == 'add' and ids:
raise PreflightError('AddRenderJob does not accept an existing job ID')
if action in ('start', 'delete') and len(ids) != 1:
raise PreflightError('Exactly one selected job is required')
project, s = snapshot(resolve, project_id, timeline_id, action == 'add')
rendering = local_scope(s, allow_running=action == 'stop')
known = {r['JobId'] for r in s['jobs']}
if any(jid not in known for jid in ids):
raise PreflightError('A selected ID is not present in this project queue')
if action == 'start' and s['statuses'][ids[0]]['job_status'] != 'Ready':
raise PreflightError('This tutorial starts only an existing local Ready job; no restart')
if action == 'cleanup' and any(s['statuses'][jid]['category'] not in ('complete', 'failed', 'cancelled') for jid in ids):
raise PreflightError('Cleanup accepts selected terminal jobs only; use single-delete for Ready')
if action == 'stop' and ids and set(ids) != set(rendering):
raise PreflightError('StopRendering is not job-scoped; supplied IDs must cover every observed Rendering job')
plan = {'action': action, 'project_id': s['project_id'], 'timeline_id': s['timeline_id'],
'selected_job_ids': ids, 'observed_rendering_ids': rendering,
'scope': 'project-wide stop' if action == 'stop' else 'explicit IDs' if ids else 'one new job',
'observed_state_hash': digest(stable(s)),
'settings_fully_read_back': False, 'atomic': False}
plan['plan_hash'] = digest(plan)
return project, s, plan
def mutate(resolve, action, project_id=None, job_ids=None, timeline_id=None,
apply=False, expect_plan=None, test_copy=False, settings_reviewed=False,
allow_project_stop=False, details=False):
project, before, plan = plan_action(resolve, action, project_id, job_ids, timeline_id)
report = {'status': 'plan', 'plan': plan, 'before': public_snapshot(before, details),
'mutations_sent': 0, 'verified_deleted_ids': [], 'file_verified': False,
'retry_allowed': False}
if not apply:
return report
text(project_id, '--project-id required for apply')
if not test_copy or not expect_plan or expect_plan != plan['plan_hash']:
raise PreflightError('Apply needs --test-copy and the exact current --expect-plan')
if action == 'add':
text(timeline_id, '--timeline-id required for AddRenderJob apply')
if action in ('add', 'start') and not settings_reviewed:
raise PreflightError('Review Deliver/job settings and acknowledge --settings-reviewed')
if action == 'stop' and not allow_project_stop:
raise PreflightError('Stop is project-wide; explicit --allow-project-stop required')
method = {'add': 'AddRenderJob', 'start': 'StartRendering', 'stop': 'StopRendering',
'delete': 'DeleteRenderJob', 'cleanup': 'DeleteRenderJob'}[action]
# Resolve callable BEFORE any sent marker; the same capability is reused.
fn = getattr(project, method, None)
if not callable(fn):
raise PreflightError('Missing mutating method: ' + method)
_, current, current_plan = plan_action(resolve, action, project_id, job_ids, timeline_id)
if current_plan != plan:
raise PreflightError('Plan changed immediately before mutation; no call sent')
if action == 'stop' and not current['is_rendering']:
report['status'] = 'already_idle_no_call'
return report
try:
if action in ('delete', 'cleanup'):
expected = stable(current)
for jid in plan['selected_job_ids']:
_, now = snapshot(resolve, project_id)
local_scope(now)
if stable(now) != expected:
report['status'] = 'partial_or_changed_before_next_call'
return report
report['attempted_job_id'] = jid
report['mutations_sent'] += 1
returned = fn(jid) # One attempt per explicitly selected ID.
_, after = snapshot(resolve, project_id)
expected['jobs'] = [r for r in expected['jobs'] if r['JobId'] != jid]
expected['statuses'].pop(jid, None)
report['after'] = public_snapshot(after, details)
if returned is not True or stable(after) != expected:
report['status'] = 'mutation_outcome_unknown'
report['return_type'] = type(returned).__name__
return report
report['verified_deleted_ids'].append(jid)
report['status'] = 'selected_deletions_verified'
return report
report['mutations_sent'] = 1
returned = fn() if action in ('add', 'stop') else fn([plan['selected_job_ids'][0]], False)
report['return_type'] = type(returned).__name__
if action == 'add' and isinstance(returned, str) and returned.strip():
report['returned_job_id'] = returned # Preserve before post-call read may fail.
_, after = snapshot(resolve, project_id, timeline_id, action == 'add')
report['after'] = public_snapshot(after, details)
if action == 'add':
jid = text(returned, 'AddRenderJob returned ID')
old_ids = [r['JobId'] for r in before['jobs']]
new_ids = [r['JobId'] for r in after['jobs'] if r['JobId'] not in old_ids]
retained = copy.deepcopy(after)
retained['jobs'] = [r for r in retained['jobs'] if r['JobId'] != jid]
retained['statuses'].pop(jid, None)
if jid in old_ids or new_ids != [jid] or stable(retained) != stable(before):
report['status'] = 'mutation_outcome_unknown'
else:
report['status'] = 'job_added_needs_settings_review'
elif action == 'start':
# A True accepts a launch request, not proof of output completion.
jid = plan['selected_job_ids'][0]
unchanged = queue_structure(before['jobs']) == queue_structure(after['jobs'])
others_ok = all(after['statuses'].get(k, {}).get('job_status') == v['job_status']
for k, v in before['statuses'].items() if k != jid)
report['status'] = ('start_accepted_needs_observation' if returned is True and unchanged and others_ok
else 'mutation_outcome_unknown')
else:
# StopRendering has no documented bool success contract; None is valid.
unchanged = queue_structure(before['jobs']) == queue_structure(after['jobs'])
rendering = any(v['category'] == 'rendering' for v in after['statuses'].values())
unknown = any(v['category'] in ('unknown', 'external') for v in after['statuses'].values())
report['status'] = ('idle_observed_after_stop' if unchanged and not after['is_rendering'] and not rendering and not unknown
else 'stop_sent_needs_observation')
report['rolled_back'] = False
return report
except (Exception, KeyboardInterrupt) as exc:
report['status'] = 'mutation_outcome_unknown'
report['error_type'] = type(exc).__name__
return report # Never retry, reverse or delete a newly added job here.
def cli_target(parser, timeline=False):
parser.add_argument('--project-id', help='Exact opaque ID; required for apply/wait')
if timeline:
parser.add_argument('--timeline-id', help='Exact current timeline ID for AddRenderJob')
parser.add_argument('--details', action='store_true', help='Include local names/paths/errors; redact before sharing')
def cli_write(parser, action):
cli_target(parser, timeline=action == 'add')
if action != 'add':
parser.add_argument('--job-id', action='append', default=[], required=action != 'stop',
help='Exact ID; repeat only for cleanup or project-stop scope check')
parser.add_argument('--apply', action='store_true')
parser.add_argument('--expect-plan', help='SHA-256 of the reviewed current plan')
parser.add_argument('--test-copy', action='store_true', help='Acknowledge isolated test project and sole operator')
if action in ('add', 'start'):
parser.add_argument('--settings-reviewed', action='store_true', help='Acknowledge manual review; not a technical verification')
if action == 'stop':
parser.add_argument('--allow-project-stop', action='store_true', help='Acknowledge non-job-scoped stop')
def write_from_args(resolve, action, args):
return mutate(resolve, action, project_id=args.project_id, job_ids=getattr(args, 'job_id', []),
timeline_id=getattr(args, 'timeline_id', None), apply=args.apply,
expect_plan=args.expect_plan, test_copy=args.test_copy,
settings_reviewed=getattr(args, 'settings_reviewed', False),
allow_project_stop=getattr(args, 'allow_project_stop', False), details=args.details)
def print_result(work):
try:
out = work()
except PreflightError as exc:
out = {'status': 'preflight_refused', 'error': str(exc), 'mutations_sent': 0}
except KeyboardInterrupt:
out = {'status': 'observer_interrupted', 'render_stopped_by_script': False}
except Exception as exc:
out = {'status': 'read_or_connection_failed', 'error_type': type(exc).__name__}
print(json.dumps(out, ensure_ascii=False, allow_nan=False, indent=2))
if out['status'] in ('plan', 'observed', 'selected_deletions_verified', 'already_idle_no_call',
'idle_observed_after_stop', 'job_complete_file_unverified'):
return 0
return 2 if out['status'] == 'preflight_refused' else 3
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.
"""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 one deletion
python delete_render_job.py --project-id "PROJECT_ID" --job-id "JOB_ID"Review the selected target and retained queue.
Apply once
python delete_render_job.py --project-id "PROJECT_ID" --job-id "JOB_ID" --apply --test-copy --expect-plan "PLAN_SHA256"No StopRendering, DeleteAllRenderJobs or retry on discrepancy.
What the code does
Read, do not guess
GetRenderJobList and GetRenderJobStatus remain separate observations. A missing field, invalid response, duplicate ID and empty queue have different meanings.
Keep the selected scope
The main script calls the downloadable shared helper below. Guards are tutorial policy, not a sandbox or a new permission system in Resolve.
Interpret the result
Exit 0 means a completed observation, plan or narrowly verified operation. Exit 2 is preflight refusal. Exit 3 means failure, uncertainty or pending review; it is not permission to retry.
Queue/status reads may expose client names, paths and errors with --details. IDs can also identify work: review the whole report before sharing; automatic redaction is not promised.
No preset changes, directory creation, all-queue start, automatic retries, rollback or project save. Native calls can block. A returned bool or matching reads do not guarantee the entire project state.
API Complete is not a media quality certificate. Verify the expected output files, decoding, duration, audio, dimensions and delivery requirements separately.
DEBUG / OBSERVATION FIRST
An error is a starting point, not a diagnosis.
Preflight refused
Possible reasonThe exact ID, plan, queue structure or required method did not match.
Next checkRead the report and your installed interface. Do not guess alternate method names or convert opaque IDs to numbers.
Unknown outcome after sending
Possible reasonA mutating call or the following read failed, or the queue changed concurrently.
Next checkRetain the returned ID and observations. Check Resolve first; never repeat the operation blindly.
Remote/background state
Possible reasonThis bounded mutation example cannot establish local exclusive control for that state.
Next checkRead the raw state with --details and use a separately qualified workflow. Do not rename it Ready just to pass the guard.
PRACTICE / YOUR OWN EVIDENCE
Verify on your own test project
- Prepare three test entries and select the middle one by its exact ID.
- Obtain a plan and verify selected_job_ids contains only that target.
- Apply once, then compare remaining IDs and information with the original report.
The target disappears and both neighbours retain their observed fields. No other mutation is called and the script does not delete media files.
This is a checklist for your test, not a report of a test already performed on the website.
FAQ
Questions that matter in practice.
Can I pass a complete GetRenderJobList dictionary?
The reviewed contract requires jobId: str. A job-information dictionary is not an equivalent argument.[18]
Why not report success when the ID is already absent?
The script verifies a selected operation, not merely the desired state. An ID absent before the call needs investigation and does not prove this script removed it.[18]
What if True is returned but the entry remains?
Treat the outcome as unverified, retain the report and inspect Resolve. Do not automatically retry DeleteRenderJob or expand to a whole-queue purge.[18]
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.
- 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.
- 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.
- 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.
- 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.
- Blackmagic DaVinciResolveScript.pyi — RenderJobInfo, RenderJobStatus and queue methods
S07 reviews a published mirror of Blackmagic’s interface on 6 October 2026: JobId/JobStatus fields and queue methods. Third-party hosting; compare your installed .pyi. Stop takes no ID and declares no success bool. Document review, not live certification.
- Python — time.monotonic and time.sleep
A monotonic clock measures elapsed time. Sleep and clock checks cannot interrupt a blocked native call; S07 uses a soft deadline plus an independent poll limit.