Mergify Merge Queue
Overview
The merge queue serializes PR merges, running CI on temporary merge commits to catch integration failures before they reach the target branch. Use comments on the PR to queue/dequeue it, and the CLI to monitor queue state, inspect individual PRs, and manage the queue.
mergify queue show <PR> reports on a PR that is no longer in the queue too — whether it was dequeued, merged by the queue, or never queued, and why — so start there for a PR that vanished from the queue. See Diagnosing a dequeued PR; the GitHub-side surfaces documented there are the fallback for when the CLI cannot read the activity log.
Queuing and Dequeuing a PR
Queue, dequeue, and requeue actions are driven by comments on the pull request, not the CLI:
| Comment | Effect |
|---|---|
@mergifyio queue |
Add the PR to the merge queue (also use to requeue a PR that was dequeued) |
@mergifyio dequeue |
Remove (dequeue) the PR from the merge queue |
@mergifyio requeue is accepted, but it is a deprecated alias that runs the same command as @mergifyio queue — there is no separate requeue behavior. Post @mergifyio queue.
When Mergify processes the comment, it adds a 👍 (thumbs up) reaction to the comment to acknowledge receipt. After queuing, use mergify queue show <PR_NUMBER> to watch the PR's status as it progresses through the queue.
Commands
mergify queue status # Show queue status (batches, waiting PRs)
mergify queue status --branch main # Filter by branch
mergify queue status --json # Machine-readable JSON output
mergify queue show <PR_NUMBER> # Detailed state of a PR in the queue
mergify queue show <PR_NUMBER> -v # Full checks table and conditions tree
mergify queue show <PR_NUMBER> --json # Machine-readable JSON output
mergify queue pause --reason "..." # Pause the queue (requires reason)
mergify queue unpause # Resume the queue
That is the whole queue group: status, show, pause, unpause. There is no subcommand for dequeuing a PR — the dequeue reason comes from queue show on a PR that has left the queue, not from a flag. For the full queue trail (every enter / checks / leave event, not just the last exit), use mergify events --pr <PR> --since 90d — see the mergify-events skill.
Is the PR queued, dequeued, or never queued?
Start here — the rest of the workflow branches on this answer.
mergify queue show <PR> answers all four cases. A PR with no queue entry is a normal answer, not an error: the command prints a notice and exits 0 in every case below.
queue show result |
Meaning |
|---|---|
PR #N block with position / CI state |
The PR is in the queue |
PR #N was dequeued <when> + a Dequeue code |
It left the queue without merging — see the reason table |
PR #N was merged by the merge queue <when> |
It left the queue by merging. Not a dequeue |
PR #N is not in the merge queue |
No queue activity in the retained window — never queued, aged out, or not a real PR number |
Under --json, a PR that is not currently queued carries queued: false plus a dequeued discriminator:
mergify queue show 1234 --json | jq -e '.queued == false' >/dev/null && echo "not in queue"
dequeued: true— left without merging; the raw leave event is underqueue_leavedequeued: false— merged by the queue, or never queued (queue_leavetells the two apart:nullmeans never queued)dequeued: null— the lookup itself failed, andqueue_leave_errorsays why. Not the same as "never queued" — fall back to the GitHub-side surfaces rather than concluding anything
queue_leave_head_sha carries the head the diagnosis describes. Compare it against the PR's current head before reporting anything from it — a dequeue is often caused by a push, so its failing checks routinely belong to a commit the PR no longer has:
mergify queue show 1234 --json | jq -r '.queue_leave_head_sha' # 31b4a485b8ce…
gh pr view 1234 --json headRefOid -q .headRefOid # 340361aae580… → superseded, stay quiet
Two limits to keep in mind. History goes back 90 days (the activity log's retention), so an older dequeue reads as "no activity". And the command does not check that the PR exists, so a typo'd number prints the same "not in the merge queue" notice — confirm the PR is real (gh pr view <PR>) before concluding it was never queued.
Do not use the presence of a Mergify Merge Queue check run as the test — a PR that merely matches the queue conditions gets one titled Waiting for queue conditions without ever being queued. A # Merge Queue Status comment is a reliable positive signal (the pre-queue "Queue this pull request" offer comment deliberately carries no such heading), but its absence proves nothing, since the comment can be disabled per repository.
Diagnosing a dequeued PR
Four surfaces carry the reason. Prefer them in this order.
1. mergify queue show <PR> (start here)
On a PR that is no longer queued, queue show reads the PR's last merge-queue exit and renders it:
PR #37823 was dequeued 24m ago
Dequeue code: CHECKS_FAILED
Queue: default
Queued at: 46m ago
Trigger: merge queue internal
Head SHA: 31b4a48
The merge conditions cannot be satisfied due to failing checks
- `@github-actions/all-greens`
Failing checks:
✗ all-greens failure
https://github.com/Mergifyio/monorepo/actions/runs/…/job/…
Fix the cause above, then comment `@mergifyio queue` on the pull request.
That is the engine's own explanation plus the failing checks with their job-log URLs — go straight to the failing job rather than hunting for it. The explanation is capped at 12 lines in compact mode; add -v for the whole thing (a PR_DEQUEUED reason embeds the full unmet-condition tree, which runs to dozens of lines).
Head SHA is the commit all of that describes. If it is not the PR's current head, the report is about a commit that has since been replaced — the checks above may already be green again. Check it before acting on the failure.
--json gives the same thing machine-readably: dequeued, plus queue_leave carrying the raw event, so read dequeue_code, reason, and unsuccessful_checks[].details_url from .queue_leave.metadata. Use the promoted queue_leave_head_sha for the staleness check above.
Reading the activity log is best-effort: a token scoped to the merge queue can be refused the repository's event log (403). The command then degrades to the plain "not in the merge queue" notice plus a warning on stderr, and reports dequeued: null — that is the signal to fall through to the surfaces below, not a statement that the PR was never queued.
2. The Mergify activity log (what surface 1 reads underneath)
GET /v1/repos/{owner}/{repo}/logs returns the queue lifecycle events, newest first. queue show calls this for you, and mergify events --pr <PR> --since 90d (the mergify-events skill) browses the whole lifecycle from the CLI — including every event type, not just queue ones. Go direct with curl only when it degrades (403 above, with a token that can read the log) or from somewhere the CLI is not installed:
REPO=owner/repo
PR=1234
FROM=$(date -u -d '90 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-90d +%Y-%m-%dT%H:%M:%SZ)
curl -sS -H "Authorization: Bearer ${MERGIFY_TOKEN:-$(gh auth token)}" \
"https://api.mergify.com/v1/repos/$REPO/logs?pull_request=$PR&event_type=action.queue.leave&received_from=$FROM" \
| jq 'if .events == [] then "no leave event in window — never queued (or aged out)"
else .events[0].metadata
| {merged, dequeue_code, reason,
failing: [.unsuccessful_checks[]? | {name, state, details_url}]}
end'
{
"merged": false,
"dequeue_code": "CHECKS_FAILED",
"reason": "The merge conditions cannot be satisfied due to failing checks\n\n- `ci-gate`",
"failing": [
{
"name": "ci-gate",
"state": "failure",
"details_url": "https://github.com/owner/repo/actions/runs/28589756829/job/84771312071"
}
]
}
Read it as:
"events": []/size: 0→ no leave event in the window → the PR was never queued (or the event aged out; see the window rule below).merged: true→ it left the queue by merging. Not a dequeue.merged: false→ it was dequeued;dequeue_codesays why (see the reason table).unsuccessful_checks[].details_url→ direct link to the failing CI job log. This is how you reach the CI failure after the PR has left the queue.
Two traps that make this silently return nothing:
received_fromis required in practice. The window defaults to the last 24 hours. A dequeue from last week returnssize: 0with no error, which reads exactly like "never queued". Always passreceived_from.- The window may not exceed 93 days (retention is 90 days) or the call fails with
422 'received_from' and 'received_to' cannot span more than 93 days.
Same endpoint, other useful filters: &outcome=failure restricts leave events to dequeues (a merge is success); drop event_type to see the whole lifecycle (action.queue.enter, checks_start, checks_end, leave).
3. The Mergify Merge Queue check run
The check-run title names the reason directly, and its summary is the full queue report:
SHA=$(gh pr view $PR --repo $REPO --json headRefOid -q .headRefOid)
gh api "repos/$REPO/commits/$SHA/check-runs" \
-q '.check_runs[] | select(.name=="Mergify Merge Queue") | {conclusion, title: .output.title, summary: .output.summary}'
Titles map to state without any parsing:
output.title |
State |
|---|---|
Dequeued — <reason> (conclusion neutral) |
Dequeued, reason in the title |
Dequeued from merge queue (conclusion neutral) |
Dequeued, but the reason did not resolve to a named code — use surface 1 |
Merged via merge queue (conclusion success) |
Merged by the queue |
Waiting for queue conditions, Checks …, In merge queue |
Still in the lifecycle |
Caveat: the check run lives on the head SHA it was written against. If the dequeue was caused by a push (PULL_REQUEST_UPDATED, DRAFT_PULL_REQUEST_CHANGED), the current head has a fresh check run and the dequeue report sits on the previous SHA. Use surface 1 or 4 in that case — surface 1 names the SHA (Head SHA / queue_leave_head_sha), so it is the one that lets you detect the mismatch rather than fall into it. Note also that gh pr view --json statusCheckRollup returns a null title — go through gh api .../check-runs as above.
4. The # Merge Queue Status comment
mergify[bot] posts one comment per queue session, so read the last one. It survives pushes, which makes it the most robust GitHub-side surface.
gh api --paginate --slurp "repos/$REPO/issues/$PR/comments" \
| jq -r '[.[][] | select(.user.login=="mergify[bot]")
| select(.body|contains("# Merge Queue Status"))] | last | .body'
--paginate matters: the endpoint returns 30 comments per page and the newest are on the last page, so without it last silently hands you a stale status comment (or none) on any PR with real discussion. --slurp collects the pages into an array of arrays — hence .[][] to flatten — and is incompatible with -q, so the filter goes through jq instead.
Its structure, in order: a hidden JSON payload, a timeline, the merge conditions, then ## Reason, Failing checks: (each with a [job log] link), and ## Hint. The hidden payload gives the state without parsing prose:
<!--- ... {"version": 1, "state": "dequeued", "queue_rule_name": "default", ...} ... -->
state is one of waiting, checking, frozen, bisecting, merged, dequeued. It does not carry the dequeue code — that is in the ## Reason prose below it.
Caveat: this comment can be turned off per repository (merge_queue.status_comments: none, or outcomes for terminal events only). Absence of a comment does not prove the PR was never queued.
Dequeue reasons and what to do next
dequeue_code values and the action they call for. The engine ships a per-reason ## Hint in the report — for a code not listed here, read that Hint rather than guessing.
dequeue_code |
What happened | What to do next |
|---|---|---|
PR_MERGED |
Merged by the queue | Nothing — this is success |
PR_MANUALLY_MERGED |
Merged outside the queue | Nothing |
CHECKS_FAILED |
Required checks failed on the merge commit | Read unsuccessful_checks[].details_url, fix the CI. Pushing a fix requeues it automatically once conditions match again; if it was flaky, requeue as-is with @mergifyio queue |
CHECKS_TIMEOUT |
checks_timeout elapsed before conditions were satisfied |
Check the reason's details: checks that never reported mean the check names in your conditions don't match what CI publishes (fix the config, not the PR). Checks still running mean CI is too slow or stuck |
PULL_REQUEST_UPDATED |
Someone pushed to the PR while it was queued | Stop pushing to a queued PR. Requeue when the branch is final |
DRAFT_PULL_REQUEST_CHANGED |
The queue's draft/batch PR got commits Mergify did not create | Never push to the merge-queue draft branch. Requeue the original PR |
CONFLICT_WITH_BASE_BRANCH |
The PR conflicts with its base branch | Rebase or merge the base branch, resolve conflicts, then requeue |
CONFLICT_WITH_PULL_AHEAD |
The PR conflicts with a PR ahead of it in the queue | Wait for the PR ahead to merge, then rebase and requeue |
BRANCH_UPDATE_FAILED |
Mergify could not update the PR's head branch | Read the reason details, update the branch yourself, requeue |
BASE_BRANCH_MISSING / BASE_BRANCH_CHANGED |
The base branch is gone or changed | Retarget the PR to a live base branch, then requeue |
PR_MANUALLY_DEQUEUED |
A human removed it (command, dashboard, or API) | The reason names who and how. Requeue only once you know why they pulled it |
PR_DEQUEUED |
Queue conditions stopped matching | Look at the conditions in the report; fix the PR or requeue |
DROPPED_BY_BISECTION_ELIMINATION |
Bisection blamed other PRs and dropped this one untested | It is unproven, not known-broken. Requeue to test it on its own |
STACK_PREDECESSOR_DEQUEUED |
A predecessor in the same stack was dequeued | Fix the predecessor, requeue the stack |
QUEUE_RULE_MISSING / CONFIGURATION_CHANGED |
The config changed under the queued PR | Fix .mergify.yml (see the mergify-config skill), then requeue |
INCOMPATIBILITY_WITH_BRANCH_PROTECTIONS |
Queue settings clash with branch protections | Reconcile the repository's branch protections with the queue config — requeuing alone will not help |
UNPROCESSABLE_PULL_REQUEST |
Too many check runs, comments, or files for Mergify to process | Shrink the PR |
Not every code means the PR left the queue. These reasons interrupt the checks and the PR stays queued — do not treat them as a dequeue and do not requeue:
PR_AHEAD_DEQUEUED, BATCH_AHEAD_FAILED, PR_WITH_HIGHER_PRIORITY_QUEUED, MERGE_QUEUE_RESET, SCHEDULED_FREEZE_STATUS_CHANGED, SPECULATIVE_CHECK_NUMBER_REDUCED, INTERMEDIATE_RESULTS_SKIPPED, CHECKS_RETRIED, BATCH_SCOPES_CHANGED, SCHEDULE_BLOCKED_AHEAD_YIELDED, PR_CHECKS_STOPPED_BECAUSE_MERGE_QUEUE_PAUSE
They arrive as abort_code on an action.queue.checks_end event (with aborted: true) rather than as dequeue_code on a leave event, and the check-run title reads Checks restarted — … or Checks aborted — … rather than Dequeued — …. The authoritative test for "did it actually leave the queue" is an action.queue.leave event with merged: false — not the presence of a code from this list.
Checking Queue Status
Use mergify queue status to see the current state of the merge queue:
- Batches: groups of PRs being tested together, shown with their CI status and ETA
- Waiting PRs: PRs queued but not yet in a batch, shown with priority and queue time
- Pause state: whether the queue is paused and why
Use --json when you need to parse the output programmatically.
Inspecting a PR in the queue
Use mergify queue show <PR_NUMBER> to check why a PR is stuck or how it's progressing:
- Position: where the PR sits in the queue
- Priority: which priority rule matched
- CI timeout: when the queue will give up on the PR's checks (
-when no timeout is configured) — watch this to catch aCHECKS_TIMEOUTbefore it fires - CI state: whether checks are passing, pending, or failing
- Conditions: which conditions are met and which are blocking
- Use
-v(verbose) for the full checks table and conditions tree
-v lists check names and states only — no links to the CI jobs. For job-log URLs on a PR that is still queued, use the GitHub-side surfaces above (the check-run summary and the status comment); once the PR has left the queue, queue show itself prints them. --json is a raw passthrough of the API payload, so it carries one more field the human render drops: queue_rule (the resolved queue rule config, not just its name).
Queue States
| State | Meaning |
|---|---|
running |
Batch is actively running CI |
preparing |
Batch is being set up |
bisecting |
Batch failed, bisecting to find the culprit |
failed |
CI failed for this batch |
merged |
PRs in this batch have been merged |
waiting_for_merge |
CI passed, waiting for GitHub to merge |
waiting_for_previous_batches |
Blocked on earlier batches completing |
waiting_for_batch |
Waiting to be picked up into a batch |
waiting_for_requeue |
A batch ahead failed; this batch will be re-embarked |
waiting_schedule |
Outside the configured merge schedule |
frozen |
Queue is paused |
Pausing and Unpausing
Pause the queue to temporarily halt all merges (e.g., during incidents or deployments):
mergify queue pause --reason "production incident — halting merges"
mergify queue unpause
- Pausing does not cancel running CI — it prevents new merges from starting
- The reason is visible to all team members in the queue status
- Use
--yes-i-am-sureto skip the confirmation prompt in scripts
Troubleshooting
PR not entering the queue:
- Make sure the PR was queued: post
@mergifyio queueand confirm Mergify reacted with 👍 on the comment - Check that the PR's merge conditions are met:
mergify queue show <PR_NUMBER> -v - Look at the conditions section for unmet requirements
- Do not assume the queue command never landed:
queue showtells a PR that was queued and dequeued apart from one that never entered
PR stuck in queue:
- Check CI state:
mergify queue show <PR_NUMBER> - If checks are failing,
-vnames them; for the job logs, read theFailing checks:links in the# Merge Queue Statuscomment or theMergify Merge Queuecheck-run summary - If the queue is paused, check who paused it:
mergify queue status
PR disappeared from the queue:
mergify queue show <PR_NUMBER>— it says whether the PR was dequeued, merged by the queue, or never queued, and prints the dequeue code, the reason, and the failing checks' URLs- Never assume "not in the merge queue" means "never queued": read the headline (or
dequeuedunder--json) before telling anyone to requeue - Then act per the reason table
Queue moving slowly:
- Check for failing batches that trigger bisection:
mergify queue status - Bisecting batches test PRs individually, which is slower than batch merging