Disclosure: I work on Renoise's marketing team. This article and its code were generated with AI assistance and are labelled AI Generated. The Python example was executed locally. It is a provider-independent teaching example, not Renoise's internal implementation or an official SDK.
A video-generation request can finish successfully and still produce an unusable shot. The bottle may change shape, the character's jacket may change color, or a camera move may hide the feature the brief asked to show.
That creates two separate questions for anyone building a creative agent:
- Did the generation job complete?
- Is its output suitable for the next step?
This tutorial builds a small Python task-plan validator that keeps those questions separate. It also binds approval to a specific task revision, so changing a prompt does not silently reuse approval for the previous version.
Start with a small workflow
Consider a product-video experiment with an opening shot and a detail shot. Both use the same product photograph. Generate and review the opening before spending credits on the detail.
The dependency here is an editorial gate, not a claim that the second clip consumes the first video's bytes. Both clips reference the same original image. If the opening reveals a bad reference or unsuitable style, you can stop before producing more footage.
A different workflow could generate an anchor image first, then make subsequent tasks consume its immutable output asset ID. In that case, resolve that ID before showing the downstream task for approval.
Separate intent from executable tasks
A brief such as “make a clean product ad” leaves too much unspecified for execution. A task record should capture the prompt, model, references, dependencies, and cost estimate.
For this compact example, the generation parameters are intentionally minimal. A real integration must include every execution-affecting parameter—duration, resolution, aspect ratio, audio settings, and seed where supported—in the approved record too.
Use versioned asset IDs or content hashes. A filename is insufficient if someone can replace the file without changing its name.
Bind approval to the task revision
A boolean such as approved=True cannot tell you whether someone approved the current prompt or yesterday's prompt. Instead, serialize the task deterministically and hash that representation. Store the approved hash against its task ID.
When the task changes, its hash changes and the approval no longer matches. This is a change detector, not authentication: a production approval service must also record who approved it and enforce authorization.
A runnable Python example
Save the following as video_task_plan.py and run it with Python 3.9 or later. It uses only the standard library and sends no network requests.
The example requires dependencies to appear earlier in the list. That is a deliberately simple ordering contract, not a general-purpose graph sorter. It rejects forward references as well as cycles.
from dataclasses import asdict, dataclass, replace
import hashlib
import json
@dataclass(frozen=True)
class Task:
id: str
prompt: str
model: str
reference_ids: tuple[str, ...]
depends_on: tuple[str, ...]
estimated_credits: int
def fingerprint(task: Task) -> str:
payload = json.dumps(asdict(task), sort_keys=True, separators=(",", ":"))
return hashlib.sha256(payload.encode()).hexdigest()
def validate(tasks: list[Task], budget: int) -> None:
seen = set()
total = 0
for task in tasks:
if task.id in seen:
raise ValueError("Duplicate task ID")
if task.estimated_credits < 0:
raise ValueError("Negative credit estimate")
if not set(task.depends_on) <= seen:
raise ValueError("Dependencies must appear earlier in the plan")
seen.add(task.id)
total += task.estimated_credits
if total > budget:
raise ValueError("Plan exceeds estimated budget")
def ready(task: Task, approvals: dict[str, str], states: dict[str, str]) -> bool:
return (
states.get(task.id, "planned") == "planned"
and approvals.get(task.id) == fingerprint(task)
and all(states.get(dep) == "accepted" for dep in task.depends_on)
)
# Fictional model names, asset IDs, and estimates; not a provider price list.
tasks = [
Task("opening", "Bottle on a neutral surface; slow push-in",
"example-video-model", ("asset:product-front:v1",), (), 20),
Task("detail", "Close-up of the same bottle lid; minimal movement",
"example-video-model", ("asset:product-front:v1",), ("opening",), 20),
]
validate(tasks, budget=40)
approvals = {task.id: fingerprint(task) for task in tasks}
states = {}
assert ready(tasks[0], approvals, states)
assert not ready(tasks[1], approvals, states)
states["opening"] = "succeeded"
assert not ready(tasks[1], approvals, states)
states["opening"] = "accepted"
assert ready(tasks[1], approvals, states)
changed = replace(tasks[1], prompt="Bottle lid, fast spinning camera")
assert not ready(changed, approvals, states)
states["detail"] = "unknown"
assert not ready(tasks[1], approvals, states)
print("All checks passed; no generation requests were sent.")
Expected output:
All checks passed; no generation requests were sent.
The assertions check five behaviors: the first approved task is eligible; a dependency blocks the second; technical success alone does not unblock it; editorial acceptance does; and changing the prompt invalidates the previous approval. An unknown task state also prevents immediate resubmission.
The approvals dictionary in the example is a test fixture. Do not populate it automatically from the plan in a real application: write an approval only after the user reviews that exact revision.
Success and acceptance are different states
A useful state sequence is:
planned -> submitting -> running -> succeeded -> accepted
| |
v v
failed rejected
Only accepted should unlock a dependency that requires creative review. Rejected means the job produced an output but that output did not meet the brief. It does not imply a provider failure or a refund.
The example's ready() function is an eligibility check, not a scheduler. Two workers can both read True. A production runner needs an atomic transition or transactional claim from planned to submitting before it sends the request.
Handle ambiguous submissions before retrying
A timeout after sending a request does not prove that the provider rejected it. The provider may have accepted the job while your client lost the response.
Mark that attempt unknown and reconcile it using a provider job ID or supported idempotency mechanism. If the provider offers neither, automatic retry can create duplicate work and charges; keep it pending for investigation instead.
Distinguish a transport retry from a creative retry. A transport retry attempts to recover the same logical request. A creative retry intentionally asks for another output. They should not share an idempotency key when the desired outcome is a new generation.
This example implements neither provider idempotency nor durable storage. Its unknown-state check simply demonstrates where execution should stop.
Treat the budget as an estimate until execution
Summing estimated_credits catches an oversized initial plan, but it is not a billing limit. Estimates can change, multiple plans can run concurrently, and retries can spend additional credits.
For production, revalidate the quote before submission and require renewed approval if it changes. Reserve budget atomically when claiming the task, then reconcile the actual charge. Persist those records so a process restart does not erase spending history.
The values 20 and 40 above are fictional units for testing the validator. They are not Renoise prices or a cost benchmark.
Applying the workflow to Renoise Agent
Renoise Agent provides a conversational workflow for preparing image, video, and audio generation tasks, with task parameters and estimated credits available for review. That makes it a concrete setting for the same operational questions: which reference is this shot using, what exactly is being generated, and should the next step proceed?
For a first product-video session, request a shot plan, review one generation at a time, and compare each result with the source product image. Preserve the approved reference and describe specific changes when requesting another attempt.
This Python program does not connect to Renoise, describe its backend, or reproduce its approval settings. It is a small model you can adapt when building your own orchestration layer around a documented generation API.
The key design decision is to make a task's inputs, approval, execution status, and creative acceptance separate pieces of state. That lets you answer why a job ran—and why another one should wait—without reconstructing the decision from a chat transcript.
Top comments (0)