Skip to content

Automatically Assigning Reviewers on GitHub

Lesson 11 of 12Intermediate → Advanced10 min readGitHub Engineering · Pull RequestsVerified: GitHub.com documentation and gh 2.98.0, August 2026

Two different problems get called “automatic reviewer assignment”, and GitHub solves them with two different features.

Automatic ownershipwho is responsible for this code? — is CODEOWNERS.

Automatic assignmentwhich specific person should do this review? — is team review assignment, an organisation team setting.

Conflating them produces a common false expectation: that CODEOWNERS distributes review fairly across a team. It does not, and it was never intended to.

CODEOWNERS requests the owners of the changed paths. If the owner is a team, the team is requested — every member is notified, and any member’s approval satisfies the requirement.

That is ownership routing. It has no concept of workload, availability, or turn-taking. If @acme/api-team owns a directory, every pull request touching it goes to the whole team, every time.

For a team of three that is fine. For a team of fifteen it produces the diffusion-of-responsibility problem: everyone is asked, so nobody is responsible, and pull requests sit.

Configured on an organisation team, review assignment replaces a team request with specific individuals, chosen by an algorithm.

AlgorithmSelection basis
Round robinWhoever received a review request least recently, rotating through members
Load balanceWhoever has had fewest review requests over roughly the last 30 days

Supporting settings:

  • Number of reviewers to assign per pull request
  • Include child team members as candidates
  • Skip specific members — for example those marked as busy
  • Count existing requests toward the total
  • Remove the team request once individuals are assigned

Round robin distributes turns; load balance distributes volume. Load balance is usually the better default because it accounts for people who were away — round robin will hand a returning colleague a review immediately, since their last request was long ago.

This is the detail worth getting right, because it determines whether your policy works.

When CODEOWNERS requests a team and that team has review assignment enabled, the team request is replaced by individual requests.

Except in one case: if a branch protection rule or ruleset requires review from code owners, the team request remains alongside the individual assignments. It has to — the requirement is defined in terms of the team, so removing the team request would make it unsatisfiable.

The practical consequence:

ConfigurationResult
CODEOWNERS onlyWhole team requested; anyone may approve
CODEOWNERS + review assignmentSpecific individuals requested; team request removed
CODEOWNERS + review assignment + required code owner reviewIndividuals requested and team request retained

The third row surprises people who enable review assignment expecting the team notification to disappear, and then find it still there. It is not a bug; it is the requirement being kept satisfiable.

The native mechanisms cover ownership and distribution. They do not cover:

  • Expertise-based routing — sending database migrations to whoever knows that subsystem
  • Availability beyond the manual skip setting — holidays, on-call rotations
  • Conditional rules — “security must review anything touching /auth/, but only for external contributors”
  • Cross-organisation assignment
  • Escalation when a review goes unanswered for two days

These need automation. The three options, in increasing order of effort:

A GitHub Actions workflow. The common choice. It reacts to pull_request events and calls the API to request reviewers according to whatever logic you write. Simple to start, and it runs with the repository’s own token.

A GitHub App. The right answer for anything used across many repositories, because permissions are explicit, bounded and auditable, and it does not consume a user’s identity. Covered in GitHub Apps.

A scheduled job calling the API. Useful for escalation — “find review requests older than 48 hours and ping” — which is a query rather than an event.

The underlying API call is straightforward in all three cases:

Terminal window
gh api --method POST repos/OWNER/REPO/pulls/PULL_NUMBER/requested_reviewers \
-f "reviewers[]=alice" \
-f "team_reviewers[]=api-team"

What it doesRequests a user and a team as reviewers on a pull request.

Why we run itThis is the primitive every reviewer-assignment automation ultimately calls. Understanding it directly makes the difference between configuring a bot and knowing what the bot does.

Expected resultThe updated pull request object. A 422 usually means the user lacks repository access or is the pull request author.

Removing a request uses the same endpoint with DELETE.

Reviewer automation fails socially more often than technically.

Assign people, not teams, for the actual review. A named individual is responsible; a team is not.

Assign few. Two reviewers on every pull request doubles the cost and rarely doubles the quality. One, with a second for high-risk paths, is usually right.

Respect availability. Assigning reviews to someone on leave delays the pull request and annoys them on return. Use the skip setting, or read status.

Make the reason visible. “Assigned because you own /services/checkout/” is actionable. An unexplained assignment feels arbitrary.

Escalate rather than reassign silently. Moving a review after 24 hours teaches everyone that requests can be ignored. A reminder is better.

Terminal window
gh pr view PULL_NUMBER --json reviewRequests,reviewDecision
gh pr list --search "review-requested:@me" --json number,title,createdAt
gh pr list --search "team-review-requested:acme/api-team" --state open

The second query is the one worth having as an alias — it is your personal review queue, and it is far more reliable than notifications.

Where native routing is not enough, a workflow is the usual answer. This one assigns a security reviewer when sensitive paths are touched:

name: Route sensitive changes
on:
pull_request:
types: [opened, ready_for_review]
permissions:
pull-requests: write
jobs:
route:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- name: Request security review for sensitive paths
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR: ${{ github.event.pull_request.number }}
AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
set -euo pipefail
files=$(gh pr diff "$PR" --name-only)
if grep -qE '^(\.github/workflows/|src/auth/|infra/)' <<<"$files"; then
if [ "$AUTHOR" != "security-bot" ]; then
gh api --method POST "repos/${GITHUB_REPOSITORY}/pulls/${PR}/requested_reviewers" \
-f "team_reviewers[]=security" || echo "could not request security team" >&2
fi
fi

Several details matter more than the logic.

The draft check. Assigning reviewers to a draft defeats the purpose of draft state.

The author check. Requesting the author as a reviewer returns 422 and fails the job.

|| echo. A 422 for any reason should not fail the workflow — a routing failure is not a reason to block a pull request.

Explicit permissions. pull-requests: write is the minimum, and declaring it prevents the workflow inheriting a broader default.

The problem native features do not address is a review request that is simply ignored.

The instinct is to reassign after a timeout. Resist it: silently moving a review teaches everyone that requests can be ignored, because they can. A reminder is better.

#!/usr/bin/env bash
set -euo pipefail
REPO="${GH_REPO:?}"
CUTOFF=$(date -u -d '2 days ago' +%Y-%m-%dT%H:%M:%SZ)
gh pr list --repo "$REPO" --state open --json number,title,createdAt,reviewDecision,reviewRequests \
--jq --arg cutoff "$CUTOFF" \
'.[] | select(.reviewDecision == null)
| select(.createdAt < $cutoff)
| select((.reviewRequests | length) > 0)
| [.number, ([.reviewRequests[].login // .reviewRequests[].name] | join(",")), .title]
| @tsv'

Post that list somewhere the team sees it. Visibility solves most of the problem, and it does so without any bot making decisions about who should do what.

NeedMechanism
Route by code ownershipCODEOWNERS
Distribute load within a teamTeam review assignment
Respect availabilityTeam assignment’s skip setting
Route by path to a specific teamCODEOWNERS, or a workflow for conditional rules
Route by change characteristicsGitHub Actions workflow
Across many repositoriesGitHub App
Escalate unanswered requestsScheduled job posting a report

Work down that table and stop at the first row that solves your problem. Most teams need the first two and build the fifth without trying them.

Expecting CODEOWNERS to load-balance. It routes by ownership only.

Enabling review assignment and expecting the team request to vanish when code owner review is required. It is deliberately retained.

Assigning whole teams for the review itself. Nobody is responsible.

Automation that dies on a 422. Authors and users without access both produce one.

Assigning three reviewers by default. Cost without benefit.

Silent reassignment on timeout. Teaches people that requests are optional.

Building a workflow before trying the native settings. Round robin and load balance solve most of the actual problem.

Before building anything, two low-tech approaches solve the problem for many teams.

A rotation. A named reviewer-of-the-day, recorded somewhere visible, who takes anything unassigned. This handles availability naturally — people swap when they are away — and it needs no tooling at all.

A review budget. An agreement that everyone reviews roughly as much as they submit. Made visible by a weekly report rather than enforced:

Terminal window
gh pr list --repo "$GH_REPO" --state merged --limit 100 --json author,reviews \
--jq '{
authored: ([.[] | .author.login] | group_by(.) | map({who: .[0], n: length})),
reviewed: ([.[] | .reviews[]? | .author.login] | group_by(.) | map({who: .[0], n: length}))
}'

Publishing that monthly changes behaviour more reliably than any assignment algorithm, because the imbalance becomes visible to the people creating it. Teams generally self-correct once they can see that one person is reviewing four times as much as anyone else.

When assignment automation is worth building

Section titled “When assignment automation is worth building”

Native settings plus a rotation cover most teams. Building something is justified when:

  • Expertise routing genuinely matters — a subsystem where the wrong reviewer adds no value.
  • The team is large enough that ownership is not obvious, and CODEOWNERS would request twelve people.
  • Compliance requires specific reviewers for specific paths, provably.
  • Review must span organisations, which native features do not do.

Not justified when the real problem is that reviews are slow. Assignment automation makes the request land on a specific person faster; it does nothing about whether they have time. Automating around a capacity problem produces the same latency with more machinery.

Whatever mechanism you use, it drifts. A quarterly check:

Terminal window
# Who is actually being requested?
gh pr list --repo "$GH_REPO" --state all --limit 100 --json reviewRequests \
--jq '[.[] | .reviewRequests[]? | (.login // .name)] | group_by(.)
| map({who: .[0], requested: length}) | sort_by(-.requested)'
# Who actually reviews?
gh pr list --repo "$GH_REPO" --state merged --limit 100 --json reviews \
--jq '[.[] | .reviews[]? | .author.login] | group_by(.)
| map({who: .[0], reviews: length}) | sort_by(-.reviews)'

Comparing the two lists is the useful part. Someone requested frequently and reviewing rarely is either overloaded or no longer working in that area — and either way the routing is producing noise rather than review, which trains everyone to ignore requests.

Requires an organisation to see the full behaviour; steps 1–2 work anywhere.

  1. Add a CODEOWNERS entry assigning a directory to a team, and open a pull request touching it.
  2. Check gh pr view --json reviewRequests and note the team was requested.
  3. Enable review assignment on that team with round robin and one reviewer.
  4. Open another pull request and confirm an individual was requested and the team request removed.
  5. Enable required code owner review, repeat, and confirm the team request is now retained alongside the individual.

Step 5 demonstrates the interaction that catches people out, and it is much clearer having seen steps 2 and 4 first.

  • Automatic ownership and automatic assignment are different problems with different features.
  • CODEOWNERS routes by path ownership and has no notion of workload.
  • Team review assignment distributes work by round robin or load balance, and is organisation-only.
  • Enabling assignment normally removes the team request — unless required code owner review is configured, when it is retained.
  • Native settings solve distribution; expertise, availability and escalation need automation.
  • Every assignment mechanism ultimately calls the requested_reviewers endpoint.

Assignment problems are usually solved by the simplest available mechanism, and reaching for automation first is the common mistake.

  1. CODEOWNERS, if the problem is that the wrong people are being asked. Path-based ownership solves routing and nothing else, which is often the whole problem.
  2. Team review assignment, if the problem is that one person receives everything. Round robin or load balance, configured in a settings page, no code.
  3. The skip setting, if the problem is availability. Cheaper than reading calendars.
  4. A rotation, written somewhere visible, if the team is small enough that tooling is overhead.
  5. A workflow, only if routing genuinely depends on something none of the above can express — which usually means the content of the change rather than its location.
  6. A GitHub App, only if this must work across many repositories.

Most teams that build something at step 5 could have stopped at step 2. The tell is whether you can state the rule in terms of paths; if you can, CODEOWNERS already does it.

And if the real complaint is that reviews are slow rather than misrouted, none of these help. Assignment changes who is asked, not whether they have time — and automating around a capacity problem produces the same latency with more machinery to maintain.

Where team review assignment is unavailable — a personal-account repository, or a rotation that must span teams — a scheduled workflow can maintain one.

name: Reviewer rotation
on:
schedule:
- cron: "0 8 * * 1" # Monday morning
workflow_dispatch:
permissions:
issues: write
jobs:
rotate:
runs-on: ubuntu-latest
steps:
- env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ROSTER: "alice bob carol dave"
run: |
set -euo pipefail
read -ra people <<<"$ROSTER"
week=$(date -u +%V)
on_duty="${people[$(( week % ${#people[@]} ))]}"
gh issue edit "$ROTATION_ISSUE" \
--body "Reviewer of the week: @${on_duty}" \
--add-assignee "$on_duty"

Deriving the index from the ISO week number rather than storing state is the useful trick: the rotation is deterministic, so a missed run does not desynchronise it and anyone can compute who is on duty without querying anything.

Pinning that Issue makes it the visible answer to “who takes unassigned reviews this week”, which is most of what a rotation needs to be.

This is deliberately simpler than reviewer assignment — it names a person rather than routing each pull request. For many teams that is sufficient, and it degrades gracefully: if the workflow stops running, the rotation is still computable from the date.

Check your understanding

4 questions — each one asks you to predict what Git or GitHub will do, not to recall a flag.

A team wants reviews spread evenly across members. Which feature does that?
Show answer

Team review assignment (round robin or load balance) — CODEOWNERS routes by path ownership and knows nothing about workload. Team review assignment is the feature that distributes work.

After enabling team review assignment, the team itself still appears as a requested reviewer. Why?
Show answer

Required code owner review is configured, so the team request is deliberately retained — Normally assignment replaces the team request with individuals. When code owner review is required, the team request is kept so the requirement can be satisfied.

Your assignment automation crashes on HTTP 422 from the requested_reviewers endpoint. What commonly causes 422 there?
Show answer

Requesting the pull request's author, or a user without access to the repository — Both cases are rejected with 422. Automation should handle it rather than die, and skip authors and non-collaborators before requesting.

What is wrong with assigning three reviewers to every pull request by default?
Show answer

It costs review time without improving outcomes, and diffuses responsibility — More reviewers is not more review. One or two named people are responsible; three by default is cost without benefit.

Professional ToolkitThe annotated CODEOWNERS template and the PR triage script are in the Professional Toolkit.