Azure DevOps scheduled pipeline: a nightly cron in UTC

Add a schedules block with a UTC cron to the pipeline YAML, set always: true to run without code changes, and key any paid API call to the date.

5 min readSume
All posts

To schedule an Azure DevOps pipeline, add a schedules: block to its YAML with a cron: expression, which Azure Pipelines reads in UTC, and the branches: it applies to. By default a scheduled run is skipped when nothing changed since the last successful scheduled run, so add always: true to a pipeline that must run every night, such as one that starts an AI video.

Azure facts come from Microsoft's Configure schedules to run pipelines and the other Azure Pipelines 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 Azure DevOps integration: the pipeline makes one plain HTTPS call with curl. The same job on GitLab is GitLab scheduled pipeline: run a nightly AI video job.

What does a scheduled pipeline look like in YAML?

Each entry under schedules: takes a five-field cron: in single quotes, a displayName, branches with include and exclude lists, and two booleans that default to false: always runs even when nothing changed, and batch stops the schedule from starting a run while an earlier run is still in progress, except when always is true. This pipeline starts one Sume Format run at 05:00 UTC every day:

  • trigger: none and pr: none turn off the default CI and PR triggers.
  • name makes the run number start with the date, which Azure DevOps Services expresses in UTC. The step builds its Idempotency-Key from that date.
  • -H @- makes curl read the header from standard input, and --fail-with-body makes it return an error on a 4xx or 5xx while still printing Sume's JSON error.
name: $(Date:yyyyMMdd).$(Rev:r) # the run number starts with the date
trigger: none
pr: none
schedules:
  - cron: '0 5 * * *' # 05:00 UTC every day
    displayName: Nightly video
    branches:
      include: [main]
    always: true # run even when nothing changed
jobs:
  - job: nightly_video
    timeoutInMinutes: 10
    steps:
      - bash: |
          DAY="${RUN_NUMBER%%.*}" # same date on every retry of this run
          printf 'Authorization: Bearer %s\n' "$SUME_API_KEY" |
            curl -sS --fail-with-body -H @- -H 'Content-Type: application/json' \
              -H "Idempotency-Key: nightly-recap-$DAY" \
              -d "{\"input\":{\"day\":\"$DAY\"},\"communication\":{\"webhook_url\":\"https://example.com/hooks/sume\"}}" \
              https://api.sume.com/v1/formats/acme/nightly-recap/runs
        retryCountOnTaskFailure: 2
        env:
          SUME_API_KEY: $(SUME_API_KEY) # a secret variable
          RUN_NUMBER: $(Build.BuildNumber)

Why didn't my scheduled pipeline run?

Start with Scheduled runs in the pipeline's context menu: it previews upcoming scheduled runs, up to seven days ahead. If the run you expect is missing, or listed but never started, Microsoft's page names these causes:

From Microsoft's Configure schedules to run pipelines, read 2026-09-28.
SymptomCauseFix
No run on quiet daysBy default the pipeline doesn't run as scheduled if nothing changed since the last successful scheduled runalways: true
The YAML schedule never runsSchedules set in the pipeline settings UI take precedence: only those runDelete the UI schedules, then push a change
One branch runs, another doesn'tA branch gets scheduled runs only if it matches the filters in the YAML file in that branchAdd it to that branch's own filters
Runs at the wrong hourCron is UTC and doesn't account for daylight saving timeConvert local time to UTC
It also runs on every pushYAML pipelines in a GitHub repository have CI and PR triggers on by defaulttrigger: none and pr: none
Runs stop appearingLimits of around 1000 runs per pipeline per week and 10 per 15 minutesSchedule less often

Where should the API key live?

In a secret variable, never in the YAML. Microsoft recommends setting secrets in the pipeline UI, in a variable group, or in a variable group linked to Azure Key Vault, and Sume's Authentication page lists CI secret stores among the places a key may live. Secrets are not decrypted into environment variables for scripts, so map the variable in the step's env:. Microsoft also says never to pass secrets on the command line, because some operating systems log command-line arguments; the example pipes the header into curl instead.

What happens if the pipeline runs twice?

Because the key comes from the date, a repeat on the same day replays the first run: Sume answers the same key and body with 200, the original receipt and idempotency_hit: true, so nothing is charged twice. Repeats happen: a manual run that day, or retryCountOnTaskFailure, which retries a failed step up to 10 times with growing waits and, in Microsoft's words, doesn't provide idempotency. Idempotency keys for AI video APIs covers the other replay cases, and a run that ended failed needs a new key before a same-day re-run, as Sume Format run failed explains. In this pipeline:

  • Keep the day's input and webhook_url fixed. The same key with a different body is 409 idempotency_conflict, and nothing runs.
  • --fail-with-body fails the step on every 4xx, so the step retry also resends answers that can't change, such as a 402 that needs funds first. Those resends are free: a 4xx at create means nothing ran and nothing was charged.

Should the pipeline wait for the video?

No. Sume answers the create at once with a receipt, while long-form video is 15 to 30 minutes of work and a run can last until 90 minutes after creation before it is finalized as failed. A job's timeoutInMinutes defaults to 60, and on Microsoft-hosted agents a private project's job can't run past 60 minutes unless extra capacity is paid for, so a polling job may be canceled first. Abandoning the wait does not stop the run or its spend.

Send communication.webhook_url, as the example does, and end the job after the create: Sume POSTs one signed format.run.terminal receipt to your server when the run completes or fails. Signed webhooks for Sume video runs covers the receiving side.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume