Auto-edit from a prompt
Describe the edit you want, attach the footage, and poll until the agent returns a finished project.
Use it when the output is an edited video rather than a single generated asset.
How it differs from the AI endpoints
| AI endpoints | The agent | |
|---|---|---|
| You give it | A model and its params | A sentence and some footage |
| It returns | One generated file | An edited project, ready to export |
| You control | Every parameter | The brief |
The script
Three steps: describe the edit, poll until it finishes, export. The agent has its own polling endpoint and status values, separate from AI jobs.
The polling helpers used below
const API_KEY = "YOUR_API_KEY";
const API = "https://api.rendley.com/v1";
const headers = {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
};
// Both endpoints share these three terminal statuses. They differ only in
// their in-progress names: queued/processing vs pending/running.
const TERMINAL = ["completed", "failed", "canceled"];
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Generation and export jobs. The finished job carries output.url.
async function waitForJob(jobId, interval = 5000) {
let job = null;
while (job === null || !TERMINAL.includes(job.status)) {
await sleep(interval);
const response = await fetch(`${API}/jobs/${jobId}`, { headers });
if (!response.ok) {
throw new Error("Job lookup failed: " + response.status);
}
const body = await response.json();
job = body.data;
console.log("Job status:", job.status);
}
if (job.status !== "completed") {
throw new Error("Job " + job.status + ": " + (job.error || ""));
}
return job;
}
// Agent jobs. The finished job carries project_id, thread_id and
// last_message, but no output: export the project to get a file.
// onPause runs when an interactive run stops to ask something.
async function waitForAgentJob(jobId, interval = 5000, onPause) {
let job = null;
while (job === null || !TERMINAL.includes(job.status)) {
// This endpoint long-polls. Sleep anyway, so a fast response
// cannot turn this into a tight request loop.
await sleep(interval);
const response = await fetch(`${API}/agent/jobs/${jobId}`, { headers });
if (!response.ok) {
throw new Error("Job lookup failed: " + response.status);
}
const body = await response.json();
job = body.data;
console.log("Edit status:", job.status);
// Only interactive runs reach this; unattended runs never pause.
if (job.status === "waiting_input" && onPause) {
await onPause(job);
}
}
if (job.status !== "completed") {
throw new Error("The edit did not finish: " + (job.error || job.reason));
}
return job;
}import time
import requests
API_KEY = "YOUR_API_KEY"
API = "https://api.rendley.com/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
# Both endpoints share these three terminal statuses. They differ only in
# their in-progress names: queued/processing vs pending/running.
TERMINAL = {"completed", "failed", "canceled"}
def wait_for_job(job_id, interval=5):
"""Generation and export jobs. The finished job carries output.url."""
job = None
while job is None or job["status"] not in TERMINAL:
time.sleep(interval)
response = requests.get(f"{API}/jobs/{job_id}", headers=HEADERS)
response.raise_for_status()
job = response.json()["data"]
print("Job status:", job["status"])
if job["status"] != "completed":
raise RuntimeError(f"Job {job['status']}: {job.get('error', '')}")
return job
def wait_for_agent_job(job_id, interval=5, on_pause=None):
"""Agent jobs. The finished job carries project_id, thread_id and
last_message, but no output: export the project to get a file.
on_pause runs when an interactive run stops to ask something.
"""
job = None
while job is None or job["status"] not in TERMINAL:
# This endpoint long-polls. Sleep anyway, so a fast response
# cannot turn this into a tight request loop.
time.sleep(interval)
response = requests.get(f"{API}/agent/jobs/{job_id}", headers=HEADERS)
response.raise_for_status()
job = response.json()["data"]
print("Edit status:", job["status"])
# Only interactive runs reach this; unattended runs never pause.
if job["status"] == "waiting_input" and on_pause:
on_pause(job)
if job["status"] != "completed":
raise RuntimeError("The edit did not finish: " + (job.get("error") or job.get("reason", "")))
return job// 1. Describe the edit and attach the footage. Returns immediately.
async function startEdit() {
const response = await fetch(`${API}/agent`, {
method: "POST",
headers,
body: JSON.stringify({
prompt:
"Cut interview.mp4 into a 60-second highlight reel. Add captions, " +
"keep the strongest quotes, and put calm background music under it.",
files: [{ url: "https://cdn.example.com/interview.mp4" }],
}),
});
if (!response.ok) {
throw new Error("Could not start the edit: " + response.status);
}
const body = await response.json();
return body.data;
}
// 2. Render the finished project to a file.
async function exportProject(projectId) {
const response = await fetch(`${API}/export`, {
method: "POST",
headers,
body: JSON.stringify({ project_id: projectId }),
});
const body = await response.json();
return body.data.job_id;
}
const started = await startEdit();
console.log("Job:", started.job_id, "Project:", started.project_id);
const edit = await waitForAgentJob(started.job_id);
console.log("Applied", edit.commands_applied, "operations");
const exportJobId = await exportProject(edit.project_id);
const finished = await waitForJob(exportJobId);
// The signed URL is the result. Fetch it, pipe it to your storage,
// or hand it to the browser.
console.log(finished.output.url);import requests
def start_edit():
"""Describe the edit and attach the footage. Returns immediately."""
response = requests.post(
f"{API}/agent",
headers=HEADERS,
json={
"prompt": (
"Cut interview.mp4 into a 60-second highlight reel. "
"Add captions, keep the strongest quotes, and put calm "
"background music under it."
),
"files": [{"url": "https://cdn.example.com/interview.mp4"}],
},
)
response.raise_for_status()
return response.json()["data"]
def export_project(project_id):
"""Render the finished project to a file."""
response = requests.post(
f"{API}/export",
headers=HEADERS,
json={"project_id": project_id},
)
response.raise_for_status()
return response.json()["data"]["job_id"]
def download(url, filename):
"""Stream the finished file to disk."""
with requests.get(url, stream=True) as response:
response.raise_for_status()
with open(filename, "wb") as out:
for chunk in response.iter_content(1 << 16):
out.write(chunk)
started = start_edit()
print("Job:", started["job_id"], "Project:", started["project_id"])
edit = wait_for_agent_job(started['job_id'])
print("Applied", edit["commands_applied"], "operations")
export_job_id = export_project(edit["project_id"])
finished = wait_for_job(export_job_id)
download(finished["output"]["url"], "edit.mp4")
print("Saved edit.mp4")Writing a good prompt
The agent runs unattended by default and never stops to ask. Your prompt is the only steering. Be specific.
| Vague | Specific |
|---|---|
| “Make it shorter” | “Cut to 60 seconds, keeping the three strongest quotes” |
| “Add music” | “Add calm instrumental music at low volume under the dialogue” |
| “Make it vertical” | “Reframe to 9:16 for Reels, keeping the speaker centered” |
Name your files as they appear in files. Uploads land in the project’s media library, not on the timeline, so tell the agent which file to use where.
Send "interactive": true to approve the plan before credits are spent. The job pauses at waiting_input, and you answer it with POST /v1/agent/jobs/{id}/respond. See Automated editing.
waiting_input is not a terminal status, so the script above would poll a paused job forever. Pass the helper’s third argument to handle the pause: waitForAgentJob(jobId, 5000, onPause), where onPause posts the answer. The script on this page leaves interactive off, so it never pauses.
Continuing the edit
Pass the same project_id and thread_id back for follow-up changes. The agent keeps the context of what it already did:
{
"prompt": "Make the intro shorter and add a call-to-action at the end.",
"project_id": "3f0d5a4e-3d2a-4c1f-9b3e-2a1c4d5e6f70",
"thread_id": "7e5b3c19-4d82-4a76-9f01-6c8d2b5a3e47"
}