Manual Jobs, Delayed Jobs, Scheduled Pipelines, Rollbacks, Freeze Windows, and Release Controls: Concepts, Architecture, and Mental Model
Add people, time, ownership, freshness, and rollback controls to the deployment model without mistaking a manual button or calendar window for complete authorization.
Learning objectives
- Separate manual, delayed, scheduled, freeze, rollback, and approval controls by the state they govern.
- Explain optional versus blocking manual jobs and the permissions attached to the target ref/environment.
- Distinguish an in-pipeline delay from server-side recurring pipeline creation.
- Explain why a deploy freeze is a time signal/policy input rather than a complete authorization system.
- Model stale-release prevention and known-artifact rollback without treating a retry button as evidence.
manual_confirmation, delayed jobs with
start_in, pipeline schedules, deployment freeze periods /
CI_DEPLOY_FREEZE, environment/deployment history, the
project setting that prevents outdated deployment jobs, and
rollback/retry controls are available on GitLab Free/Premium/Ultimate
across GitLab.com, Self-Managed, and Dedicated. Protected environments
and deployment approvals—the fine-grained “only these people may
deploy” layer—remain Premium/Ultimate. The mandatory chapter path uses
only a disposable project, synthetic artifacts/environments, and tiny
jobs. No production endpoint, cloud account, paid tier, runner
registration, real credential, or release publication is required.
1. The practical problem: release automation needs both motion and restraint
Chapters 10–17 built the mechanics of GitLab CI/CD: jobs run on runners, rules decide existence, variables cross trust boundaries, artifacts carry identity, and deployments create environment history. A production release adds a second question: even if a deployment job can run, should it run now, under this actor, for this commit, against this environment?
Teams usually need several kinds of restraint. A person may need to make a conscious decision. A rollout may need to wait. A recurring maintenance pipeline may need to exist even when nobody pushes code. A holiday freeze may discourage routine releases. And if an older deployment is still waiting after a newer one succeeds, the old path must not silently overwrite the new state.
The mistake is to call all of these things “approval.” They are different controls with different failure modes.
2. Mental model: five clocks and identities around one deployment
flowchart TD C[Commit + artifact identity] -- pipeline --> P[Pipeline] P -- optional human decision --> M[Manual job] P -- elapsed time --> D[Delayed job] S[Pipeline schedule owner + cron] -- creates --> P2[Scheduled pipeline] F[Freeze period] -- sets CI_DEPLOY_FREEZE --> G[Deployment rules / policy] M --> X[Deployment job] D --> X G --> X X -- environment record --> E[(Environment / deployment history)] N[Newer deployment] -. stale-path check .-> X R[Known artifact / previous commit] -- new recovery deployment --> E
The commit/artifact identity answers what. The pipeline source and schedule owner answer why/under whose permissions the pipeline exists. Manual/delayed state answers when the job may start. Freeze logic adds a time-policy signal. Stale-deployment controls answer whether an older job is still safe. Rollback creates another deployment action; it does not erase the failed/newer history.
3. Manual jobs: a human start condition, not automatically a strong approval policy
A job with when: manual exists in the pipeline but
waits for a person to start it. GitLab currently requires the actor
to have permission to merge to the assigned branch. For a sensitive
protected environment, Premium/Ultimate protected-environment rules
can narrow the allowed deployers further.
There are two commonly confused manual-job defaults:
| Definition | Default allow_failure |
Pipeline behavior | Typical use |
|---|---|---|---|
when: manual outside rules |
true |
Optional manual job; pipeline can complete without it. | Optional diagnostics, optional promotion, cleanup. ... |
rules: ... when: manual |
false |
Blocking manual job by default; pipeline is blocked until someone runs it. | A deliberate release gate when policy allows it. |
optional_smoke:
stage: verify
script: echo "synthetic smoke"
when: manual
blocking_release_gate:
stage: deploy
script: echo "synthetic release gate"
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: manual
allow_failure: false
Make the behavior explicit instead of relying on defaults. A reviewer should be able to tell whether skipping the job blocks the pipeline by reading the YAML.
4. Confirmation reduces accidental clicks, not malicious authority
manual_confirmation adds an explicit confirmation step.
It is useful for destructive or sensitive manual jobs because the
operator must acknowledge the action. It does
not change who is authorized to run the job.
deploy_synthetic:
stage: deploy
script: echo "would deploy known artifact"
when: manual
manual_confirmation: "Deploy the reviewed synthetic artifact?"
For fine-grained “only this operations group may deploy production,” use protected environments where available, plus target-system IAM. A confirmation dialog is not role separation.
5. Manual parameters: job inputs are safer than ad-hoc secret variables
Current GitLab lets manual jobs be run with job-specific variables and, in recent versions, typed job inputs. Manually supplied variables are visible to other project members who can rerun the job under documented project-visibility rules, can override existing variables, and are not a good place to paste credentials. Use validated inputs for non-secret configuration and external/managed secrets for credentials.
6. Delayed jobs: elapsed time inside an already-created pipeline
when: delayed plus start_in creates a job
now but schedules its start later. The supported delay is from one
second through one week. The timer begins when the job’s stage
becomes eligible; a failed prior stage prevents the timer from
progressing into execution.
timed_canary:
stage: deploy
script: echo "synthetic delayed rollout"
when: delayed
start_in: 30 seconds
A delayed job is still tied to the original pipeline and commit. That is precisely why stale-code risk exists: while it waits, a newer pipeline may deploy a later commit. A time delay is not a freshness guarantee.
Operators can Unschedule a delayed job to stop its timer, then run it manually if appropriate. That action should be recorded as an exception, not treated as invisible routine.
7. A subtle syntax rule: start_in moves inside
rules
If a job uses rules, put both
when: delayed and start_in in the matching
rule. Defining start_in at job level while using
rule-driven when causes CI configuration validation to
fail.
deploy_later:
stage: deploy
script: echo "delayed synthetic deployment"
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: delayed
start_in: 2 minutes
- when: never
8. Pipeline schedules create new pipelines; they do not delay an existing one
A pipeline schedule is a server-side object with a cron pattern,
target ref, owner, time zone, active state, and optional
inputs/variables. At each scheduled time GitLab creates a
new pipeline whose source is schedule.
This is appropriate for periodic maintenance, nightly validation, or
routine release checks independent of code pushes.
The schedule runs with the schedule owner’s permissions. If that owner is blocked or removed, the schedule can become inactive. Maintainers/Owners can take ownership. A manual “Run” of a schedule uses the triggering user’s permissions instead of the schedule owner’s permissions.
| Control | Identity anchored to | Clock anchored to | If code changes while waiting |
|---|---|---|---|
| Delayed job | Original pipeline + original SHA | Elapsed delay after stage becomes eligible | Still points to old pipeline/SHA unless stale-deploy controls stop it. |
| Pipeline schedule | Schedule owner + target ref resolved for each run | Cron + time zone | Each occurrence creates a fresh pipeline at the then-resolved ref. |
9. A schedule is a delegated identity object
Because a schedule uses its owner’s permissions, review ownership just as you review service identities. A schedule targeting a protected branch requires the owner to retain the relevant merge permission; protected tags require permission to create that tag. Prefer pipeline schedule inputs for configuration because they are typed/validated and recommended by current GitLab documentation; do not store long-lived credentials in schedule variables.
For the mandatory lab, you create an inactive disposable schedule or use a fixture. That proves the schedule object and owner/ref/cron state without starting recurring compute.
10. Deploy freeze: GitLab emits a time-window signal; your pipeline enforces behavior
Deployment freeze periods are Free across all offerings. A
Maintainer/Owner can define cron-style start/end windows and a time
zone. When a pipeline is created during a freeze window, GitLab
exposes the pre-pipeline variable
CI_DEPLOY_FREEZE=true.
This distinction matters: the freeze window does not replace authorization and should not be described as a magic firewall. Your rules/policy must decide what the deployment job does when the variable is present.
deploy_release:
stage: deploy
script:
- echo "synthetic deployment only"
rules:
- if: '$CI_DEPLOY_FREEZE'
when: manual
allow_failure: true
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: manual
allow_failure: false
- when: never
During a freeze, this example converts routine deployment into an explicit optional exception path. A production organization should also define who may invoke exceptions, where the reason is recorded, and how target-system authorization works.
11. Prevent outdated deployment jobs: freshness is a first-class release invariant
GitLab’s project setting Prevent outdated deployment jobs stops an older deployment job from overwriting a deployment that GitLab considers newer. An outdated job fails with an explicit reason, and an outdated manual deployment can have its Run button disabled.
Job “age” for this safety mechanism is based on job start ordering,
not simply commit date. That nuance is why you should inspect
deployment/job IDs and timestamps rather than assuming “newer SHA
always wins.” Combine the setting with
resource_group when one environment must be mutated by
only one job at a time.
12. Rollback: create a new controlled deployment of known state
Rollback is a release action, not time travel. GitLab can retry/redeploy an older deployment under configured rollback behavior, but sensitive projects should be cautious with retries. Current GitLab deployment-safety guidance recommends disabling pipeline retries in sensitive projects and, when rollback is required, running a new pipeline with the previous commit.
The stronger invariant from Chapters 16–17 remains: deploy the same known artifact identity if possible. Rebuilding an old commit against mutable package repositories can produce different bytes and is therefore not evidence of rollback.
13. A manual button is not a change-management system
A production release gate generally needs more context than “someone clicked Play”:
| Evidence | Question answered |
|---|---|
| MR/review/commit SHA | What code was approved? |
| Artifact digest | What exact bytes are being promoted? |
| Manual/schedule actor | Who caused this release path to proceed? |
| Environment/deployment ID | Where did GitLab record the deployment? |
| Freeze/exception record | Was normal release timing suspended or bypassed? |
| Runner/job token/target IAM | What execution identity had authority? |
| Post-deploy verification | Did the external target actually reach the intended state? |
Chapter 24 will cover GitLab Release objects/tags/evidence in depth. Here, “release controls” means gating the operational deployment path, not publishing a Release object.
14. Read-only inspection before changing any release control
PROJECT_ID="12345678"
# Inspect project CI/deployment safety settings (selected fields only).
glab api "projects/$PROJECT_ID" --jq '{id,path_with_namespace,default_branch,ci_forward_deployment_enabled,ci_forward_deployment_rollback_allowed}'
# Existing pipeline schedules: owner/ref/cron only.
glab api "projects/$PROJECT_ID/pipeline_schedules" --paginate \
--jq '.[] | {id,description,ref,cron,cron_timezone,active,owner:.owner.username,next_run_at}'
# Existing freeze periods: time-policy metadata only.
glab api "projects/$PROJECT_ID/freeze_periods" --paginate \
--jq '.[] | {id,freeze_start,freeze_end,cron_timezone}'
# Recent deployments: preserve SHA/job ordering before touching stale paths.
glab api "projects/$PROJECT_ID/deployments?order_by=finished_at&sort=desc" \
--jq '.[] | {id,status,sha,ref,environment:.environment.name,job:(.deployable.id // null),finished_at}'
Do not dump all pipeline variables, schedule variables, job environments, or tokens. Inspection should answer the release-control question with the minimum metadata necessary.
15. DevOps connection: release safety is state-machine design
A reliable release path makes invalid transitions difficult: unreviewed code should not become a deployable artifact; a stale delayed job should not overwrite a newer deployment; a freeze should turn routine automation into a controlled exception path; a schedule should have a current owner; and rollback should preserve artifact identity. These are state-machine invariants, not UI preferences.
Knowledge check
What is the default difference between
when: manual outside rules and inside
rules?
Outside rules the manual job is optional by default (allow_failure: true); inside rules it is blocking by default (allow_failure: false). Make the choice explicit.
Does manual_confirmation narrow who is authorized
to deploy?
No. It reduces accidental execution. Fine-grained deployer authorization uses branch/environment/role controls and, for protected environments, Premium/Ultimate policy.
How is a delayed job different from a pipeline schedule?
A delayed job belongs to an already-created pipeline/SHA and waits elapsed time; a schedule is a server-side cron object that creates new pipelines.
What does a deploy freeze give the pipeline?
A time-window signal: CI_DEPLOY_FREEZE=true when
the pipeline runs during a configured freeze. Pipeline policy
must decide how to respond.
Why can a delayed deployment become unsafe even if its timer is correct?
A newer pipeline may deploy first, making the old delayed job stale. Use stale-deployment prevention, serialization, and identity checks.
Why is rebuilding an old commit a weaker rollback than redeploying a known artifact?
Mutable dependencies can make the rebuild produce different bytes, so the rollback identity is no longer proven.
Summary
Manual jobs model human start conditions; delayed jobs model elapsed time inside one pipeline; pipeline schedules create recurring new pipelines under an owner identity; deploy-freeze periods provide a policy signal; stale-deployment prevention protects environment freshness; and rollback must preserve known state. These controls compose—they do not substitute for ref protection, environment authorization, runner isolation, artifact identity, or target IAM.
Official references
- GitLab Docs — Control how jobs run
- GitLab Docs — CI/CD YAML syntax reference
- GitLab Docs — Scheduled pipelines
- GitLab Docs — Pipeline schedules API
- GitLab Docs — Deployment safety
- GitLab Docs — Deployments
- GitLab Docs — Environments
- GitLab Docs — Freeze Periods API
- GitLab Docs — Predefined CI/CD variables
- GitLab Docs — Pipeline settings
- GitLab Docs — Pipelines
- GitLab Docs — Job troubleshooting
- GitLab Docs — Resource groups
- GitLab Docs — Protected environments
- GitLab Docs — Deployment approvals
- GitLab Docs — Job token
- GitLab Docs — Roles and permissions
- GitLab 19.3 — What’s new
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.