Cloud Scheduler trigger for a Cloud Run job, retry-safe

Add a Cloud Scheduler trigger to a Cloud Run job with a cron and a time zone. A failed task retries 3 times by default, so key paid calls to the date.

5 min readSume
All posts

To run a Cloud Run job on a schedule, add a Cloud Scheduler trigger to it: on the job's Triggers tab, choose Add Scheduler Trigger and give it a unix-cron frequency and a time zone, or create a Cloud Scheduler HTTP job that POSTs to the job's :run URL. A job consists of one or more tasks, and a failed task restarts up to 3 times by default, so a task that calls a paid API must key that call to the date: then every restart replays the same request instead of paying again.

Google Cloud facts come from Execute jobs on a schedule and the other Google pages under Sources; Sume facts come from Create a run, Runs and results and Errors and spend. All were read on 2026-09-28. Sume has no Google Cloud integration: the task makes one plain HTTPS call. The AWS version of this pattern is How to run a Lambda function on a schedule.

How do I create the Cloud Scheduler trigger?

Enable the Cloud Scheduler API first (gcloud services enable cloudscheduler.googleapis.com). You need Cloud Scheduler Admin, or a custom role with cloudscheduler.jobs.create, plus Cloud Run Invoker to execute jobs with the gcloud CLI. In the console, open the job, click the Triggers tab, then Add Scheduler Trigger:

  • Name and region: the Scheduler job's region does not need to match the Cloud Run job's.
  • Frequency in unix-cron format, for example 0 12 * * *, and a Timezone.
  • A service account with permission to invoke the job.
gcloud scheduler jobs create http daily-recap-trigger \
  --location=us-central1 \
  --schedule="0 6 * * *" \
  --time-zone="America/New_York" \
  --uri="https://run.googleapis.com/v2/projects/PROJECT_ID/locations/us-central1/jobs/daily-recap:run" \
  --http-method=POST \
  --oauth-service-account-email=PROJECT_NUMBER-compute@developer.gserviceaccount.com

Why can a scheduled job call the API more than once?

Because a restarted task runs your code again, POST included. A container that exits non-zero has failed, and Cloud Run restarts the task up to the job's maximum retries. With retries on, the task timeout applies to each attempt, and an attempt that doesn't finish in time is stopped. Cloud Scheduler can retry its own call if you configure it, and a person can also execute the job from the console or the gcloud CLI.

From Google Cloud's maximum retries, task timeout and Cloud Scheduler retry pages and the gcloud reference, read 2026-09-28.
SettingDefaultRangeApplies to
Maximum retries30 to 10Each task, not the whole job
Task timeout10 minutesUp to 168 hours (1 hour with GPUs)Each attempt of a task
Scheduler --max-retry-attempts00 to 5The Scheduler's own call
Scheduler --time-zoneEtc/UTCA tz database nameThe schedule

What should the task send to the paid API?

One create keyed to the UTC date, with a body that is also fixed for the day. On a restart, Sume answers the same key and body with 200, the original receipt and idempotency_hit: true, so no second run starts. Idempotency keys for AI video APIs covers the other replay cases, and a run that ended failed needs a new key, as Sume Format run failed explains. Don't build the key from CLOUD_RUN_TASK_ATTEMPT: that counter starts at 0 and goes up on every retry, so each restart would become a new paid run. And schedule the trigger well away from midnight UTC, so a restart can't compute the next day's date.

Store the API key in Secret Manager, which Google recommends for API keys, grant the job's service identity Secret Manager Secret Accessor, and expose the key with gcloud run jobs update daily-recap --set-secrets SUME_API_KEY=sume-api-key:1. Google recommends pinning a version for environment variables, which are resolved at instance startup. urlopen raises HTTPError on an error status, the error still carries its status code and body, and the exit code tells Cloud Run the task failed.

import datetime, json, os, sys, urllib.error, urllib.request

day = datetime.datetime.now(datetime.timezone.utc).date().isoformat()
body = {
    "input": {"day": day},  # fixed for the day: same key, same body
    "communication": {"webhook_url": "https://example.com/hooks/sume"},
}
req = urllib.request.Request(
    "https://api.sume.com/v1/formats/acme/daily-recap/runs",
    data=json.dumps(body).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['SUME_API_KEY']}",
        "Content-Type": "application/json",
        "Idempotency-Key": f"daily-recap-{day}",  # unchanged on every restart
    },
    method="POST",
)
try:
    with urllib.request.urlopen(req, timeout=30) as resp:
        run = json.load(resp)["data"]  # 202: new run, 200: replay
except urllib.error.HTTPError as err:
    print(err.code, err.read().decode())  # Sume's JSON error
    sys.exit(1)  # non-zero: the task failed and may restart
print(run["id"], run["idempotency_hit"])

Should the task wait for the video to finish?

No. Long-form video is 15 to 30 minutes of work, longer than the default 10-minute task timeout, and an attempt that runs out of time is stopped. Stopping the wait never stops the run or its spend. Submit, print the run id, and exit 0. Sume POSTs one signed format.run.terminal receipt to communication.webhook_url when the run completes or fails, so any public HTTPS endpoint you run can take the result; Signed webhooks for Sume video runs shows the receiving side. If you poll instead, a run is finalized as failed at most 90 minutes after creation, so a task timeout above that covers the whole run.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume