Jobs & idempotency
Two things take long enough to need care: rendering video, and publishing to someone else's platform. Both have machinery you should understand before you write a retry loop.
Renders are jobs
A video render takes one to several minutes. Rather than hold a connection open, generating tools submit to a durable server-side queue and poll it. The MCP tools do this for you — render_ad and generate_video return the finished URL — so most callers never touch the queue directly. list_jobs and get_job exist for resuming and inspecting.
Job statuses
There are exactly four, and they mean what they say:
| Status | Meaning |
|---|---|
queued | Admitted, not yet dispatched — or scheduled for a future time. |
running | A worker owns it. progress is a 0→1 float. |
done | Finished. result is populated before the status flips, so a poll can never see done with an empty result. |
error | Terminal. error carries a human-readable message. |
Polling
The reference client polls every 3 seconds with a 10-minute deadline and no backoff. A job that completes on the final tick still resolves — the deadline is checked after the status branch, not before.
A job's result is wrapped. Workers return { data: … }, so the payload you want is job.result.data when that key is present. Unwrap it rather than assuming a shape.
Concurrency and admission
- Four jobs run at once per server process.
- An account may have 16 jobs in flight; a seventeenth is refused with
429rather than queued indefinitely. - Immediate submissions are credit-checked on admission and refused with
402if the balance cannot cover the estimate. A job scheduled for the future is exempt from both — it is checked when it runs.
Restarts, and why a failed render is not retried for free
Jobs are leased. A worker stamps ownership when it picks a job up and renews it while working; if a process dies, another adopts the job only once the lease has genuinely expired. That is what stops two instances rendering — and billing — the same job twice.
Recovery is deliberately selective. Pure render jobs are re-queued after an interrupted restart. Publishing jobs are not, ever: replaying a publish would post twice, and a duplicate post is not something an automatic retry may decide to risk.
Publishing idempotency
A publish can succeed on the platform and still fail to reach you — a transport timeout, a dropped connection, a crashed agent. A caller that retries then double-posts; a caller that trusts the error tells the user their live post failed. Neither is acceptable, so publishing runs through an idempotency layer.
The claim is written before the vendor call
Not after. The window where this goes wrong is the upload, so a claim written afterwards protects nothing.
Two ways to be covered
| How | Window | Use it when |
|---|---|---|
You pass an idempotencyKey | 24 hours | You control the retry. The key is your promise that these two calls are the same publish, so no time limit is needed. |
| Nothing — a fingerprint is derived | 10 minutes | Automatic. Two identical posts 90 seconds apart are a retry; two identical posts a day apart are a legitimate re-post, and the window is what tells them apart. |
Four outcomes
| Verdict | What happens |
|---|---|
proceed | Nothing matched. The publish runs. |
replay | This already succeeded. You get the original result and nothing is posted again. |
inflight | The first attempt is still running. 409, nothing posted — ask again shortly. |
unknown | An earlier attempt's outcome could not be determined. 409, reported as neither success nor failure. |
What releases a claim, and what keeps it
The asymmetry is the whole design: when we cannot tell, we assume the irreversible thing happened.
- Released — a clean
4xxfrom the platform, or one of our own validation refusals. These provably published nothing, so a corrected retry sails straight through. - Kept — a timeout, a socket reset, a
5xx, an unclassifiable throw, and explicitly a408or429. A request timeout and a throttle can both sit on top of work that landed.
If you have checked the platform yourself and know the post did not go out, allowDuplicate is the stated escape hatch. It is the only one, on purpose.
Long publishes
post_to_meta takes async: true, which routes the publish through the durable queue so a large video upload has no transport timeout to hit. Raising a timeout is not a fix — it moves the cliff.