RESOLVE API / QUEUE LAB / 08

How to delete one Render Job from the queue

Remove one entry, not the project’s entire list.

Delete one exact ID from an idle queue using a reviewed plan, target-disappearance and neighbour checks. No preparatory render stop or speculative argument conversions.

THE RESULT

One selected removal is verified, or an uncertain outcome is reported without another attempt.

PYTHON / RESOLVESTUDY MODEL
01target_id = "JOB_ID"

Verify this exact string exists in the intended project. A filename does not select the target.

CHECK

One existing ID.

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

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.

02

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.

03

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.

ProjectDeleteRenderJob(job_id)
Input
One nonempty string ID.
Returns
Bool.
Check
One attempt; never substitute the whole entry object.
ProjectGetRenderJobList()
Input
Before planning and after the call.
Returns
RenderJobInfo list.
Check
Only the target disappears; observed neighbours remain.
ProjectIsRenderingInProgress()
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.

Three Python files in the same folder

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_render_job.py
"""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.

resolve_api_queue.py
"""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.

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 one deletion

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

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

  1. Read, do not guess

    GetRenderJobList and GetRenderJobStatus remain separate observations. A missing field, invalid response, duplicate ID and empty queue have different meanings.

  2. 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.

  3. 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.

Boundaries to keep

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

  1. Prepare three test entries and select the middle one by its exact ID.
  2. Obtain a plan and verify selected_job_ids contains only that target.
  3. Apply once, then compare remaining IDs and information with the original report.
ACCEPT WHEN

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.

  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. 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.

  6. 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.

KEEP BUILDING

Continue with a precise next task.

API / Render

How to manage the render queue with Python

Follow a precise JobId from queue inspection to creation, launch, observation and output review. No fallback that starts every job and no hidden queue purge.

API / Render

How to clean the Render Queue without deleting other jobs

Selectively remove completed, failed or cancelled Render Jobs by explicit IDs, with a plan, neighbour checks and immediate stop on a partial or uncertain outcome.

API / Render

Render Settings: exact keys and a reviewable request

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.

Back to the API library →MCP and AI assistants →