Deno.cron weekly Sume video run with a per-day idempotency key

A Deno.cron job that starts one Sume Format run each Monday, keyed by UTC date so a double fire replays, skips overlap, and hands the result to a webhook.

5 min readSume
All posts

To start a weekly Sume video from your own code, register a Deno.cron handler that sends POST /v1/formats/{handle}/{slug}/runs with an Idempotency-Key built from the date, on_active_run: "skip", and a communication.webhook_url. The key means a cron that fires twice on the same day replays the first run instead of paying for a second. The webhook means the handler returns in seconds and Sume calls you when the run finishes.

This is the do-it-yourself option. Sume's own Scheduled agents also run on a cron expression, but you create those in the dashboard, and the Developer API can list, read and start them without being able to create or edit them. If you want the schedule in your repository, a Format run from your own scheduler is the route the docs describe.

The handler

SUME_FORMAT is handle/slug. The Format reads product_url from input, which Sume hands the agent as data, not as instructions.

const API = Deno.env.get("SUME_BASE") ?? "https://api.sume.com/v1";

Deno.cron("weekly-sume-video", "0 9 * * 1", async () => {
  const slot = new Date().toISOString().slice(0, 10); // one run per UTC day
  const res = await fetch(`${API}/formats/${Deno.env.get("SUME_FORMAT")}/runs`, {
    method: "POST",
    signal: AbortSignal.timeout(30_000),
    headers: {
      Authorization: `Bearer ${Deno.env.get("SUME_API_KEY")}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `weekly-video-${slot}`,
    },
    body: JSON.stringify({
      instruction: "Make this week's 9:16 product spot.",
      input: { product_url: Deno.env.get("PRODUCT_URL") },
      generation_spend_cap_usd: 5,
      on_active_run: "skip",
      communication: { webhook_url: Deno.env.get("HOOK_URL") },
    }),
  });
  const body = await res.json();
  if (!res.ok) throw new Error(`${res.status} ${body.error?.code}`);
  console.log(body.data.id, body.data.status, body.data.idempotency_hit);
});

Why each field is there

Idempotency-Key is derived from the slot, weekly-video-2026-10-12 for example, and not from a random value. Same key with the same body returns 200 with the original receipt and idempotency_hit: true, with no second charge. The same key with a different body returns 409 idempotency_conflict, so if you change PRODUCT_URL between two fires on one day, the second one fails loudly instead of rendering.

on_active_run: "skip" records a skipped run when a run of the Format is already in flight. A skipped run costs nothing. It is already terminal on the create response, and it never sends a webhook, so the handler logs data.status to make the skip visible. The default is allow, which runs concurrently. Scheduled agents default to skip, which is why the docs warn not to copy their bodies into a Format call without thinking about it.

generation_spend_cap_usd is the ceiling for this one run, up to $500. Leave it off and the run inherits the Format's own cap, which is $400 when the Format never set one.

What the handler logs and what it means (read 2026-10-07)
Logged `status``idempotency_hit`Meaning
queuedfalseA fresh run started; wait for the webhook
queued or processingtrueA second fire on the same UTC day replayed the first run
skippedfalseAnother run of this Format was in flight; nothing was spent

The receiving side

The webhook carries one signed format.run.terminal delivery when the run completes or fails, and never for canceled or skipped. Verify the sume-v1 HMAC against the raw body before parsing, and dedupe on request_id, which repeats across retries. If your endpoint is down for ten attempts, the run stays completed; fetch it from result_url and replay the delivery with POST /v1/format-runs/{run_id}/webhook/redeliver.

Treat the webhook as a hint plus a handoff, not as the only record. Store the run id from the log line, and have the receiver fetch the full receipt from result_url if its own copy is missing. A run that is completed can still have an output you must validate against your own schema before it reaches a storefront.

Deno.cron runs only while your Deno process or deployment is alive and registered. If the handler throws, as it does here on a non-2xx response, nothing retries the call on its own. The next slot is a different key.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume