Two different problems get called “automatic reviewer assignment”, and GitHub solves them with two different features.
Automatic ownership — who is responsible for this code? — is CODEOWNERS.
Automatic assignment — which 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.
What CODEOWNERS does
Section titled “What CODEOWNERS does”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.
What team review assignment does
Section titled “What team review assignment does”Configured on an organisation team, review assignment replaces a team request with specific individuals, chosen by an algorithm.
| Algorithm | Selection basis |
|---|---|
| Round robin | Whoever received a review request least recently, rotating through members |
| Load balance | Whoever 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.
How they interact
Section titled “How they interact”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:
| Configuration | Result |
|---|---|
| CODEOWNERS only | Whole team requested; anyone may approve |
| CODEOWNERS + review assignment | Specific individuals requested; team request removed |
| CODEOWNERS + review assignment + required code owner review | Individuals 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.
When native features are not enough
Section titled “When native features are not enough”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:
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.
Designing assignment that people accept
Section titled “Designing assignment that people accept”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.
Checking what happened
Section titled “Checking what happened”gh pr view PULL_NUMBER --json reviewRequests,reviewDecisiongh pr list --search "review-requested:@me" --json number,title,createdAtgh pr list --search "team-review-requested:acme/api-team" --state openThe second query is the one worth having as an alias — it is your personal review queue, and it is far more reliable than notifications.
A worked Actions assignment
Section titled “A worked Actions assignment”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 fiSeveral 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.
Escalation rather than reassignment
Section titled “Escalation rather than reassignment”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 bashset -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.
Choosing an approach
Section titled “Choosing an approach”| Need | Mechanism |
|---|---|
| Route by code ownership | CODEOWNERS |
| Distribute load within a team | Team review assignment |
| Respect availability | Team assignment’s skip setting |
| Route by path to a specific team | CODEOWNERS, or a workflow for conditional rules |
| Route by change characteristics | GitHub Actions workflow |
| Across many repositories | GitHub App |
| Escalate unanswered requests | Scheduled 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.
Common mistakes
Section titled “Common mistakes”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.
Balancing load without automation
Section titled “Balancing load without automation”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:
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.
Reviewing the routing itself
Section titled “Reviewing the routing itself”Whatever mechanism you use, it drifts. A quarterly check:
# 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.
Exercise
Section titled “Exercise”Requires an organisation to see the full behaviour; steps 1–2 work anywhere.
- Add a CODEOWNERS entry assigning a directory to a team, and open a pull request touching it.
- Check
gh pr view --json reviewRequestsand note the team was requested. - Enable review assignment on that team with round robin and one reviewer.
- Open another pull request and confirm an individual was requested and the team request removed.
- 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.
What you learned
Section titled “What you learned”- 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.
The order to try things
Section titled “The order to try things”Assignment problems are usually solved by the simplest available mechanism, and reaching for automation first is the common mistake.
- 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.
- Team review assignment, if the problem is that one person receives everything. Round robin or load balance, configured in a settings page, no code.
- The skip setting, if the problem is availability. Cheaper than reading calendars.
- A rotation, written somewhere visible, if the team is small enough that tooling is overhead.
- 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.
- 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.
A scheduled rotation
Section titled “A scheduled rotation”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 rotationon: 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.
Related lessons
Section titled “Related lessons”Check your understanding
4 questions — each one asks you to predict what Git or GitHub will do, not to recall a flag.