Supabase cron Edge Function: start an AI video on a schedule

Schedule a Supabase Edge Function with pg_cron and pg_net, keep the video API key in the function's secrets, and start one date-keyed run per day.

6 min readSume
All posts

To run a Supabase Edge Function on a cron schedule, enable Supabase Cron (the pg_cron extension) and pg_net, then schedule a job whose SQL calls net.http_post on the function's URL, with the URL and key read from Supabase Vault. For an AI video, the function, not the SQL, holds the video API key: it starts the run with a date-keyed Idempotency-Key and a webhook URL, and returns.

Supabase facts come from its Scheduling Edge Functions, Cron, pg_net, and Edge Functions pages under Sources; Sume facts come from Create a run, Runs and results, and Authentication. All were read on 2026-09-27. Sume has no Supabase connector: the function makes a plain HTTPS call. The receiving side is in Supabase Edge Function webhook for Sume.

How do I enable cron in Supabase with a SQL query?

Supabase Cron runs on the pg_cron extension, and pg_net lets a job make HTTP requests. Enable both with the SQL below, or enable pg_cron from the Cron module under Integrations in the Dashboard. Two cautions from Supabase: the pg_net API is in beta, so function signatures may change, and disabling pg_cron permanently deletes all jobs.

create extension pg_cron with schema pg_catalog;
grant usage on schema cron to postgres;
grant all privileges on all tables in schema cron to postgres;
create extension pg_net with schema "extensions";

How do I schedule an Edge Function with pg_cron?

Supabase's recipe stores the project URL and key in Vault, then schedules net.http_post against the function. The version below adds a cron_secret: Supabase calls the publishable key safe to use in a browser, so holding it proves nothing about the caller. Job names are case sensitive, and a second job with the same name overwrites the first. Supabase's examples annotate schedules in GMT, so 0 6 * * * is 06:00 GMT. net.http_post waits 2000 ms by default; Supabase's own sub-minute example sets timeout_milliseconds to 5000.

select vault.create_secret('https://project-ref.supabase.co', 'project_url');
select vault.create_secret('YOUR_SUPABASE_PUBLISHABLE_KEY', 'publishable_key');
select vault.create_secret('a-long-random-string', 'cron_secret');

select cron.schedule(
  'daily-recap-video',
  '0 6 * * *', -- 06:00 GMT every day
  $$
  select net.http_post(
    url:= (select decrypted_secret from vault.decrypted_secrets where name = 'project_url')
      || '/functions/v1/start-daily-video',
    headers:= jsonb_build_object(
      'Content-Type', 'application/json',
      'apikey', (select decrypted_secret from vault.decrypted_secrets where name = 'publishable_key'),
      'x-cron-secret', (select decrypted_secret from vault.decrypted_secrets where name = 'cron_secret')
    ),
    timeout_milliseconds:= 5000
  ) as request_id;
  $$
);

What should the Edge Function send to the video API?

Set SUME_API_KEY and CRON_SECRET with supabase secrets set. Secrets are available immediately, with no redeploy, and a name cannot start with SUPABASE_. The function keeps the starter template's key check, adds its own secret check, and starts one run of a saved Sume recipe, a Format. The key and the body come from the UTC date alone, so a repeat that day sends an identical request. Store data.id against the date: 202 is a new run, 200 a replay of the day's run.

import { withSupabase } from "npm:@supabase/server@^1";

export default {
  fetch: withSupabase({ auth: ["publishable", "secret"] }, async (req) => {
    const cronSecret = Deno.env.get("CRON_SECRET");
    if (!cronSecret || req.headers.get("x-cron-secret") !== cronSecret) {
      return new Response("forbidden", { status: 403 });
    }
    const day = new Date().toISOString().slice(0, 10); // UTC date
    const res = await fetch("https://api.sume.com/v1/formats/acme/daily-recap/runs", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${Deno.env.get("SUME_API_KEY")}`,
        "Content-Type": "application/json",
        "Idempotency-Key": `daily-recap-${day}`,
      },
      body: JSON.stringify({
        input: { day },
        communication: { webhook_url: "https://project-ref.supabase.co/functions/v1/sume-webhook" },
      }),
    });
    const { data, error } = await res.json();
    if (!res.ok) return Response.json(error, { status: res.status });
    return Response.json({ run_id: data.id, replay: data.idempotency_hit });
  }),
};

Why not call the video API straight from SQL?

pg_net can POST JSON, but it fits this job badly. Every job is stored on the cron.job table, so a key typed into the command sits in a table, while Sume's docs say to keep API keys on trusted servers. net.http_post is also asynchronous: it returns only a request id, and the response lands in net._http_response, kept for 6 hours by default. The SQL never sees the run id it should store; the function does.

What if the job fires twice or the call times out?

The date key makes any repeat that day safe. Two of Sume's idempotency rules matter here; Idempotency keys for AI video APIs covers the rest:

  • Same key, same body: 200 with the original receipt and idempotency_hit: true. No second run, no second charge.
  • Same key, different body: 409 idempotency_conflict, and nothing runs. The webhook_url is part of the body, so keep it fixed.
  • To see what happened, read cron.job_run_details, which records every job run and its status, and net._http_response, which has the function's status_code and a timed_out flag.
From Supabase's Cron, pg_net, and Edge Function limits pages and Sume's Runs and results, read 2026-09-27.
PieceLimit or default
Supabase CronSupabase recommends no more than 8 jobs running at once, each for no more than 10 minutes
net.http_post2000 ms default timeout; responses kept 6 hours
Edge Function wall clock150 s on Free, 400 s on paid plans
Long-form Sume video15 to 30 minutes of work
Sume run deadlineAt most 90 minutes after created_at, then force-finalized as failed

Should the function wait for the video?

No. Long-form video takes longer than either Edge Function wall clock in the table above, and a function that stops watching does not stop the run or its spend. communication.webhook_url gets one signed POST when the run completes or fails, so a second Edge Function receives the result; Supabase Edge Function webhook for Sume shows it with verify_jwt = false and an HMAC check. The same trigger pattern on Cloudflare is Cloudflare Workers cron job.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume