GitLab scheduled pipeline: run a nightly AI video job

Run a GitLab scheduled pipeline nightly, start the AI video with a date-based Idempotency-Key, and poll inside a job timeout above the run deadline.

6 min readSume
All posts

A GitLab scheduled pipeline runs your .gitlab-ci.yml on a cron pattern you set under Build > Pipeline schedules, and a job can limit itself to those runs with rules: - if: $CI_PIPELINE_SOURCE == "schedule". For a nightly AI video, that job starts one run with an Idempotency-Key built from the pipeline's date, reads the API key from a masked CI/CD variable, and polls with backoff inside a job timeout set above the run's 90-minute deadline.

GitLab facts come from its docs listed under Sources, starting with Scheduled pipelines; Sume facts come from Create a run, Runs and results, and the Format cookbook. All were read on 2026-09-27. Sume has no GitLab integration: the job makes plain HTTPS calls with curl and jq. The same flow on GitHub, triggered by a release, is GitHub Actions: generate a release video.

How do I create a scheduled pipeline in GitLab?

In the project, select Build > Pipeline schedules, then New schedule. The form takes:

  • An interval pattern: a preset, or any cron value, though schedules cannot run more often than the instance's maximum scheduled pipeline frequency.
  • The target branch or tag the pipeline runs on.
  • Inputs, up to 20, and CI/CD variables that exist only in this schedule's pipelines. GitLab recommends inputs over variables for pipeline configuration.
  • You become the schedule owner, and the pipeline runs with your permissions. Select Run to start it now; manual runs are allowed once per minute.

Where should the API key live?

In a project CI/CD variable, never in the YAML. Sume's docs list CI secret stores among the places a key may live. GitLab's Visibility setting defaults to Masked since GitLab 18.3, which replaces the value with [MASKED] in job logs; a masked value must be a single line with no spaces and at least 8 characters. GitLab also warns that masking is not a guaranteed way to keep a value from malicious users. Selecting Protect variable limits it to pipelines on protected branches or tags, so point the schedule at a protected branch.

What does the nightly job look like?

The key and the body come from CI_PIPELINE_CREATED_AT, the pipeline's creation time in ISO 8601, UTC by default, so every retry inside that pipeline sends the same request. The image needs curl and jq. GitLab reports only the last command's result when commands share one string, so each failure path ends in an explicit exit 1. The loop backs off from 5 to 60 seconds, as Sume's cookbook recommends for a CI job that cannot expose an endpoint, and prints on every pass.

nightly-video:
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
  timeout: 100 minutes # above Sume's 90-minute run deadline
  retry: 2
  script:
    - |
      DAY="${CI_PIPELINE_CREATED_AT%%T*}" # the pipeline's UTC date, same on every retry
      jq -n --arg day "$DAY" '{input: {day: $day}}' > body.json
      curl -sS -X POST https://api.sume.com/v1/formats/acme/nightly-recap/runs \
        -H "Authorization: Bearer $SUME_API_KEY" -H "Content-Type: application/json" \
        -H "Idempotency-Key: nightly-recap-$DAY" -d @body.json -o run.json
      RUN_ID=$(jq -er .data.id run.json) || { cat run.json; exit 1; }
    - |
      SLEEP=5
      while :; do
        STATUS=$(curl -sS "https://api.sume.com/v1/format-runs/$RUN_ID" \
          -H "Authorization: Bearer $SUME_API_KEY" | jq -r '.data.status // "unknown"')
        echo "$(date -u +%T) $STATUS" # steady output: silent jobs are dropped after an hour
        case "$STATUS" in
          completed) break ;;
          failed|canceled|skipped) exit 1 ;;
        esac
        sleep "$SLEEP"; SLEEP=$(( SLEEP < 60 ? SLEEP * 2 : 60 ))
      done

How long can a GitLab job run while it waits?

Set the job timeout above Sume's deadline so GitLab does not fail the job first. If it stops anyway, the run keeps executing and spending; read it later by its id. A 429 or 503 during the loop is transient, which is why an unreadable status just means another pass. If you do run a public HTTPS endpoint, send communication.webhook_url and end the job after the create; Sume POSTs the result there once the run completes or fails.

From GitLab's Customize pipeline configuration and YAML reference and Sume's Runs and results, read 2026-09-27.
LimitValueDocumented by
Project job timeout60 minutes by default; 10 minutes or more, under one monthGitLab
Job timeout keywordCan be longer than the project timeout, never longer than the runner'sGitLab
Project and runner timeouts both setThe lower value winsGitLab
Job with no outputDropped after one hour, regardless of the timeoutGitLab
Run deadlineAt most 90 minutes after created_at, then force-finalized as failedSume
Long-form video15 to 30 minutes of workSume

What happens when the job is retried?

Without retry, GitLab does not retry a job; with retry: 2, a failed job is processed up to two more times, for any failure type by default. Each retry sends the same key and body, so Sume answers 200 with the original run and idempotency_hit: true: no second run, no second charge, and the loop picks up the same run. A manual Run of the schedule on the same UTC date replays that run too.

Two cases need a new key. A run that ended failed stays bound to its key, so retry it with a new one. And for a deliberate second video on the same date, add a version you bump, such as nightly-recap-$DAY-v2. Idempotency keys for AI video APIs covers key design.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume