Controlled approvals instead of automatic deploys
when: manual and allow_failure solve two different problems in GitLab CI: one controls who is allowed to trigger a step, the other whether a failure blocks the pipeline. Combined correctly, they produce safe rollout gates for production deploys and optional extra steps that never hold anyone up.
Table of Contents
- 1. Why not every job should run automatically
- 2. when: manual basics: jobs that wait for a click
- 3. allow_failure basics: optional steps without blocking
- 4. Combining when: manual and allow_failure for optional manual steps
- 5. Production deploy as a rollout gate with protected environments
- 6. Manual rollback jobs that don't clutter the pipeline
- 7. Difference between top-level when: manual and rules: with when: manual
- 8. Setting timeout and expire_in sensibly for manual jobs
- 9. Conclusion: design approval logic deliberately instead of combining at random
- 10. Summary
- 11. FAQ
1. Why not every job should run automatically
A standard GitLab CI pipeline runs from stage to stage as soon as the previous job succeeds. For build and test steps that is exactly the desired behavior: every commit should be checked automatically without anyone having to press a button. But once steps have real, often irreversible consequences, such as deploying to a production server or deleting resources, a fully automatic flow becomes risky. A broken merge that happens to pass every test would otherwise go live unchecked.
GitLab CI offers two independent, but frequently combined, controls for this: when: manual pauses a job until someone explicitly starts it from the pipeline view, and allow_failure decouples a job's success from the pipeline's overall status. Both keywords solve different problems, but in practice they often complement each other into robust approval and rollout patterns.
2. when: manual basics: jobs that wait for a click
Setting when: manual removes a job from the automatic flow. It appears in the pipeline graph as a play-button icon and waits until a user with sufficient permissions starts it manually. Until then the pipeline stays pending on that branch of the graph, but does not necessarily block subsequent automatic jobs in other stages, unless those explicitly depend on the manual job via needs:.
In the example, deploy-production is a classic manual job: it only runs in the deploy stage, is tied to environment: production, and is not triggered automatically after tests succeed. Only a team member who considers the changes ready clicks Play in the GitLab UI. That creates a deliberate human checkpoint exactly where automated safety alone is not enough.
deploy-production:
stage: deploy
environment:
name: production
url: https://shop.example.com
script:
- ./deploy.sh production
when: manual
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
3. allow_failure basics: optional steps without blocking
allow_failure: true reverses the effect of a failed job: instead of marking the entire pipeline as failed, GitLab flags the job itself with a yellow warning icon while the pipeline overall still counts as successful and subsequent stages start normally. This is ideal for steps whose result matters but should not block, for example a security scan whose findings get triaged manually, or a linter whose warnings the team deliberately does not treat as a hard gate criterion.
In the example, the security-scan job is allowed to fail without turning the merge request status red. The team still sees the warning clearly in the pipeline widget and can review it deliberately, instead of every finding automatically blocking every merge. It is important to use allow_failure: true deliberately and not as a blanket fix for flaky tests, since that lets real failures slip through unnoticed.
security-scan:
stage: test
script:
- composer audit --format=json
allow_failure: true
artifacts:
reports:
dependency_scanning: gl-dependency-scanning-report.json
4. Combining when: manual and allow_failure for optional manual steps
Combining both keywords on one job produces a step that neither starts automatically nor, if run and it fails, blocks the pipeline. This suits extra steps that should only run on demand, for example an expensive performance test before a major release that nobody wants triggered automatically on every ordinary merge, but whose failure also should not be a reason to block the merge.
In the example, performance-test waits to be started manually, and even a failure does not turn the pipeline red. That is deliberately different from deploy-production: there, a failure should indeed turn visibly red, because a failed deployment is a real problem. Whether allow_failure fits a manual job therefore depends on whether a failure of that specific step should really be a blocker.
performance-test:
stage: test
script:
- k6 run --vus 50 --duration 2m loadtest.js
when: manual
allow_failure: true
5. Production deploy as a rollout gate with protected environments
For real production deploys, when: manual alone is often not enough, because GitLab by default lets any user with the Developer role trigger manual jobs. The complement is a protected environment: under Project Settings > CI/CD Settings > Protected Environments, it can be configured that only users with the Maintainer role or an explicitly named group may start the job for environment: production. That keeps when: manual technically simple while access control is enforced through GitLab's permissions instead of mere convention.
Adding needs: further enforces that a manual deploy job only becomes startable after certain prior jobs have succeeded, for example a smoke test on the staging environment. That creates a multi-stage gate: automated tests must be green, and only then does a human with sufficient permission decide on the final step.
smoke-test-staging:
stage: verify
environment:
name: staging
script:
- ./smoke-test.sh staging
deploy-production:
stage: deploy
needs: ["smoke-test-staging"]
environment:
name: production
url: https://shop.example.com
script:
- ./deploy.sh production
when: manual
6. Manual rollback jobs that don't clutter the pipeline
A rollback job should exist in every pipeline but practically never get triggered. With when: manual it stays available, unused, in the background without disrupting the normal flow. If it is needed after all, for example because a deployment caused unexpected errors in production, a team member can start it directly from the same pipeline without creating a new pipeline or redeploying an old commit.
allow_failure: true also makes sense here, though for a different reason than with the performance test: if the rollback job is never triggered, it stays in the pipeline status as skipped or manual and does not negatively affect the overall status anyway. But if it is triggered and fails itself, for example because the previous deployment artifact is no longer available, that should not retroactively mark the whole pipeline as failed while the team is already manually working on the actual problem.
rollback-production:
stage: deploy
environment:
name: production
action: stop
script:
- ./deploy.sh production --rollback-to=$PREVIOUS_STABLE_TAG
when: manual
allow_failure: true
7. Difference between top-level when: manual and rules: with when: manual
When when: manual is set directly at the job level, it applies to every pipeline in which the job runs at all, regardless of branch or trigger. In many cases, though, a job should run automatically on main but stay manual on feature branches, or not appear at all for scheduled pipelines. That can only be expressed via rules: with conditional when: manual, since rules: allows a separate when: per condition.
In the example, deploy-production only gets the chance to be triggered manually on the main branch at all; on every other branch the job does not appear in the pipeline because no rules: condition matches. That is more precise than a blanket job-level when: manual, which would display the job on every branch, even where a production deploy never makes sense in the first place.
deploy-production:
stage: deploy
environment:
name: production
script:
- ./deploy.sh production
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
- when: never
8. Setting timeout and expire_in sensibly for manual jobs
Manual jobs can in theory wait indefinitely for their trigger, which becomes a problem with artifacts: if a previous job's build output has long since been deleted via expire_in before anyone starts the manual deploy. Too short an expire_in causes a deploy job to fail because the required artifact no longer exists; too long an expire_in wastes storage in the project unnecessarily.
For production deploy artifacts, an expire_in of several days up to two weeks has proven reasonable, depending on how long can realistically pass between a successful build and the actual approval. A per-job timeout: can additionally be set in case the script itself might hang, for example a deployment waiting on an unreachable external resource.
9. Conclusion: design approval logic deliberately instead of combining at random
when: manual and allow_failure answer two separate questions: who is allowed to trigger a step, and does a failure block the pipeline. Applying both thoughtlessly to every critical job, without considering the consequence of each, leads either to needlessly blocked pipelines or to silently overlooked production failures.
The safe approach is to decide explicitly for every job: should it run automatically or manually, and should a failure turn visibly red or only appear as a warning. Combined with protected environments and needs: dependencies, this produces rollout gates that enforce real human control at the right points without losing the pipeline's overall degree of automation.
| Combination | Pipeline status on failure | Start behavior | Typical use |
|---|---|---|---|
| when: on_success (default) | Pipeline fails | Automatic after prior stage | Build, unit tests |
| when: manual, no allow_failure | Pipeline fails | Waits for manual start | Production deploy |
| when: manual + allow_failure: true | Pipeline stays green | Waits for manual start | Optional tests, rollback |
| allow_failure: true, no manual | Pipeline stays green | Automatic after prior stage | Security scan, linter |
Mironsoft
CI/CD pipelines, zero-downtime deployments and release automation
Deployments that run without downtime and without the nail-biting?
We review existing GitLab pipelines for fragile deployment steps and missing safeguards, then build a release process with zero-downtime deployments, automated checks and a rollback you can actually trust in an emergency.
Pipeline Review
Checking an existing .gitlab-ci.yml for fragility, missing stages and security gaps.
Zero-Downtime Deployment
Building symlink releases, health checks and rollback strategies for Magento stores.
CI/CD Automation
Connecting tests, security scans and deployments into one reliable pipeline.
10. Summary
when: manual and allow_failure: The Essentials at a Glance
when: manual
Pauses a job until an authorized user explicitly starts it from the pipeline view.
allow_failure
Decouples a failed job from the pipeline's overall status, which still counts as successful.
Protected environments
Enforce that only authorized roles can trigger manual deploy jobs for protected environments.
rules: over job level
Conditional when: manual per branch or trigger is more precise than a blanket job-level setting.