Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for GitLab CI/CD By James Joyner IV · · 8 min read

Scheduled Pipelines in GitLab: Nightly Builds and Cron Jobs Done Right

GitLab pipeline schedules turn your CI into a reliable cron with audit trails. Here's how I run nightly tests, dependency updates and cleanups without surprises.

  • #gitlab
  • #cicd
  • #pipelines
  • #automation
  • #cron
  • #devops

Every team eventually accumulates a pile of recurring jobs: nightly integration tests against the full dataset, a weekly dependency-update run, a daily cleanup of stale environments, a Monday-morning report. The lazy default is to scatter these across a server’s crontab, where they run invisibly until the box reboots and nobody notices for three weeks.

GitLab’s pipeline schedules are a far better home for this work. You get the same pipeline machinery your code uses — logs, retries, notifications, an audit trail of every run — driven by a cron expression. I’ve migrated a lot of orphaned cron jobs into scheduled pipelines, and the visibility alone is worth it. Here’s how to do it well.

What a pipeline schedule actually is

A pipeline schedule (under Build → Pipeline schedules, or via the API) is a saved cron expression plus a target ref and a set of variables. At each tick, GitLab starts a pipeline on that ref exactly as if someone had pushed — but with CI_PIPELINE_SOURCE == "schedule", which is the hook you use to make scheduled runs behave differently from push runs.

That predefined variable is the whole trick. Your .gitlab-ci.yml already runs on every push; you don’t want your nightly full-suite job firing on every commit. So you gate jobs on the source:

nightly-integration:
  stage: test
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
  script:
    - ./run-full-integration-suite.sh

Now that job runs only when the schedule triggers it, never on a normal push. Conversely, your fast unit tests can exclude schedule runs if they’re redundant overnight.

Distinguish multiple schedules with a variable

The moment you have more than one schedule on a repo, you need to tell them apart. The clean way is a custom variable set on each schedule. Create a “nightly” schedule with SCHEDULE_TASK=nightly and a “weekly-deps” schedule with SCHEDULE_TASK=deps, then branch on it:

nightly-tests:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TASK == "nightly"'
  script: ./run-full-integration-suite.sh

dependency-update:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TASK == "deps"'
  script:
    - ./update-deps.sh
    - ./open-mr-if-changed.sh

One repo, one .gitlab-ci.yml, several independent scheduled tasks, each cleanly addressable. This scales far better than trying to encode the task in the cron timing and reverse-engineering “what is supposed to run at 3am on Tuesdays.”

Common scheduled workloads

A few patterns I reach for constantly:

Nightly full test suite. The expensive end-to-end and load tests you can’t afford to run on every MR. Run them overnight against main so you catch regressions while they’re still fresh, not at release time.

Automated dependency updates. A scheduled job that bumps dependencies, runs the suite, and opens an MR if anything changed. This keeps you off the “we haven’t updated anything in a year” cliff without a human remembering to do it.

Environment and resource cleanup. Tear down stale review apps, prune old artifacts, expire orphaned cloud resources. Cleanup is exactly the kind of work that never happens unless it’s automated.

Cache and registry warming. Pre-build and push your common base images overnight so the morning’s pipelines start warm.

Schedules need a person who owns them

Here’s the operational reality: a scheduled pipeline runs as the user who owns the schedule. If that person leaves the company and their account is deactivated, the schedule silently stops or fails on permissions. I’ve debugged “why did our nightly tests stop running” exactly this way more than once.

Two defenses. Own critical schedules with a service account rather than a personal one, so they survive staff changes. And enable notifications on failure so a broken schedule pages someone instead of rotting:

nightly-tests:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TASK == "nightly"'
  script: ./run-full-integration-suite.sh
  # let pipeline emails / Slack integration surface failures

Wire the project’s integrations (Slack, email) to alert on failed pipelines, and treat a red scheduled pipeline with the same seriousness as a red MR pipeline. A schedule nobody watches is worse than no schedule, because it creates false confidence that the work is happening.

The gotchas

  • Schedules don’t run on a deactivated or protected-ref-less owner. Owner needs the right to run pipelines on the target ref. Schedule against a protected branch and you need a maintainer-level owner.
  • Cron is in a timezone — check which one. GitLab schedules carry a timezone setting. “Midnight” in the wrong zone runs during business hours.
  • Catch-up runs. If your instance was down at the scheduled tick, the run is skipped, not queued for later. Don’t assume a missed window self-heals.
  • Don’t put the cron logic in .gitlab-ci.yml. The timing lives in the schedule UI/API; the behavior lives in rules. Keeping them separate means changing a time is a click, not a commit.

Where to go from here

Pipeline schedules turn invisible, unaudited crontab entries into first-class CI runs with logs, retries, and alerting. Gate jobs on CI_PIPELINE_SOURCE == "schedule", distinguish multiple schedules with a SCHEDULE_TASK variable, own critical schedules with a service account, and make failures loud. Do that and your recurring work becomes something you can actually trust.

For more on automating GitLab pipelines, see our GitLab CI/CD guides. And when reviewing the scripts your scheduled jobs run unattended, our AI code review assistant helps catch the silent-failure modes that bite hardest when no human is watching.

Schedule ownership and timezone behavior depend on your GitLab version and configuration. Verify your setup with a low-stakes schedule first.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.