Production engineering guide for rfc process design covering patterns, implementation strategies, and operational best practices.
RFC process design defines the machinery that moves a proposal from inception through review, approval, implementation, and closure. It is distinct from the content of individual RFCs—those are decided by authors—and from the bodies that evaluate them, such as Architecture Review Boards. This page concerns itself with the workflow states, transition rules, template structures, assignment mechanisms, and lifecycle management that make an RFC process functional or dysfunctional.
This design matters when an organization has outgrown ad-hoc “send it to Slack” decision-making but has not yet settled on a repeatable, measurable path for change. It matters when RFCs pile up in an indeterminate state, when reviewers cannot determine what is expected of them, or when leadership cannot discern which proposals have been approved, which are pending, and which have been abandoned. A well-designed RFC process reduces coordination overhead, makes dependency mapping legible, and provides a auditable trail for why systems evolved as they did.
The following sections are organized by what you are trying to accomplish. Each section offers concrete patterns—commands, configuration shapes, code snippets—that you can map to the tools your team actually uses. No vendor-specific mechanisms are attached to any example; everything described is a pattern you can implement on top of git, markdown, HTTP, or your platform’s issue tracker.
Every RFC process must map a proposal through a sequence of states. The most common set consists of: draft, proposal, under review, approved, implemented, and closed. Some organizations add states such as deferred, rejected, or superseded. The design choice here is not merely semantic; it determines what actions are permitted in each state, who may trigger transitions, and what metadata is required at each step.
A minimal state machine can be expressed in markdown front matter, then enforced by tooling or manual process. Consider this template skeleton:
---
title: "Add dark mode support to the dashboard"
status: draft
owner: @alice
reviewers:
- @bob
- @carol
created: 2024-02-01
---
## Summary
Replace the dashboard’s color palette with a high-contrast dark variant.
## Motivation
User testing revealed that 30% of power-users prefer working in low-light environments.
## Risks
- May require updating the design system’s color tokens.
- No breaking changes expected.
## Decision Needed
Approve the color palette swap and assign a frontend owner.
The status field is the single source of truth for where the RFC lives in its lifecycle. Transitions between states should be gated—either by a script, a pull request merge policy, or a manual checkpoint. A common failure mode is allowing anyone to change status from draft to proposal without review, which creates RFCs that skip validation and enter the approval pipeline prematurely.
If you are using GitHub, the status field can be mirrored with issue labels (draft, proposal, under-review, approved). If you are using GitLab, the same pattern applies via MR labels. The mechanism is always the same: a label or front-matter field that downstream tooling reads to determine permitted actions.
Transition guards are the rules that prevent invalid state changes. A guard might read: “Only a designated approver may transition an RFC from under review to approved.” Guards can be implemented as GitHub branch protection rules, GitLab merge request approvals, or custom CI checks.
A concrete guard pattern using GitHub checks involves a small script that reads the RFC’s status field and fails the check if the transition is unauthorized. Here is a bash example that could be run as a GitHub Action:
#!/usr/bin/env bash
set -euo pipefail
RFC_FILE="$(git diff --name-only HEAD~1 HEAD | grep -E '^README|^RFC' | head -1)"
if [[ -z "$RFC_FILE" ]]; then
echo "No RFC file changed; exiting."
exit 0
fi
STATUS=$(grep -oP 'status: \K\S+' "$RFC_FILE" || true)
if [[ -z "$STATUS" ]]; then
echo "No status field found in $RFC_FILE."
exit 1
fi
CURRENT_USER=$(git config user.name)
# Example guard: only @dave may approve
if [[ "$STATUS" == "approved" && "$CURRENT_USER" != "dave" ]]; then
echo "ERROR: Only dave may approve RFCs. Current user: $CURRENT_USER"
exit 1
fi
echo "Status check passed. Current status: $STATUS"
exit 0
Place this script as .github/checks/rfc-status-check.yml and reference it in your pull request template. The script reads the status field, validates the actor, and exits with a non-zero code to block the merge. If your team uses GitLab, the equivalent is a pipeline rule that checks the same fields before allowing a merge.
The sharp edge here is that guards are only as reliable as their maintenance. If an approver leaves the team and no one updates the script, the guard either never fires (allowing unauthorized transitions) or fires incorrectly (blocking legitimate work). Document the guard’s intent and assign ownership of its update.
The template is the structured entry point for every RFC. A poorly designed template either omits critical information, forcing reviewers to chase the author, or is so burdensome that authors skip sections or produce shallow submissions. The design pattern that works across teams is to separate mandatory fields from optional guidance.
Mandatory fields typically include:
Optional sections provide context without requiring completion. These might include:
A concrete markdown template that enforces the mandatory shape could look like this:
# {{TITLE}}
**Status**: {{STATUS}}
**Owner**: {{OWNER}}
**Created**: {{CREATED}}
## Summary
{{SUMMARY}}
## Decision Needed
{{DECISION_NEEDED}}
{% if MOTIVATION is defined %}
## Motivation
{{MOTIVATION}}
{% endif %}
{% if RISKS is defined %}
## Risks
{{RISKS}}
{% endif %}
Notice the use of template placeholders ({{TITLE}}, {{STATUS}}, etc.) rather than hard-coded fields. These placeholders are parsed by your chosen tooling—whether a static site generator, a wiki renderer, or a custom script—to populate the final displayed RFC. The pattern of using placeholders for mandatory fields and conditional inclusion for optional ones is portable across any system that can process markdown front matter.
A frequent confusion is between the Summary and Decision Needed fields. The Summary describes *what* is being proposed; the Decision Needed specifies *what the reviewer must decide*. Vague Decision Needed sections—such as “Please review”—lead to abandoned RFCs because reviewers cannot determine their scope of responsibility. A well-written Decision Needed reads like a question: “Approve the migration of the auth service to JWT, or specify alternative token strategies?”
Reviewer assignment is where many RFC processes break down. Three patterns compete: flat assignment (any interested party may comment), designated pool (a rotating set of reviewers), and role-based assignment (reviewers are selected by role, not identity).
Flat assignment works for small teams or low-velocity processes, but it silently fails when no one comments because everyone assumes someone else will. The designated pool pattern assigns a fixed set of reviewers per RFC, often documented in the template’s reviewers field. Role-based assignment ties reviewer selection to a team or function—e.g., “frontend team review all UI-related RFCs”—and is typically enforced by tooling rather than manual process.
If your team uses GitHub, reviewer assignment can be automated via CODEOWNERS. A CODEOWNERS file entry such as:
/rfcs/* @team/frontend
ensures that any pull request touching an RFC under the /rfcs/ path requires approval from the @team/frontend team. This is not a magic bullet; CODEOWNERS only blocks merges if the required reviewers have not approved. If the team is large and reviews are infrequent, the block may persist for weeks.
A sharper edge with CODEOWNERS is that it applies to the entire path, not just RFC content. If your repository contains other directories under /rfcs/—such as design assets or meeting notes—those files inherit the same ownership requirements, potentially creating unnecessary friction. Scope your CODEOWNERS entries precisely to the RFC markdown files themselves.
If you are using GitLab, the equivalent pattern is reviewers in the merge request description, which can be parsed by CI to add required reviewers as approvers. A GitLab CI job might read the reviewers field from the MR description and add those users to the approval list:
reviewers:
script: |
#!/usr/bin/env bash
set -euo pipefail
DESCRIPTION=$(cat "$CI_MERGE_REQUEST_DESCRIPTION_PATH")
echo "$DESCRIPTION" | grep -oP 'reviewers: \K.*' | tr ',' '\n' | while read -r user; do
gitlab-reviewers add "$user"
done
Again, the pattern is generic: read a field from the MR description, act on it. The specific implementation—whether a bash script, a pre-merge hook, or a GitHub Action—depends on your toolchain.
A common failure mode in reviewer assignment is the “set-and-forget” pattern: reviewers are assigned at RFC creation but never reassigned if the RFC’s scope changes. If an RFC’s owner changes mid-process, the original reviewer list may no longer be appropriate. Design your process to allow reviewer list updates as a transitional step, perhaps via a “reassign reviewers” sub-command or a manual checkpoint in the lifecycle.
Once RFCs are in flight, status tracking ensures visibility. Manual tracking—“ask the owner in standup”—scales poorly. Automated tracking reads the status field and surfaces it in dashboards, Slack channels, or email digests.
A lightweight automation pattern uses a script that queries your version control system for RFC files and their status values, then posts a summary to a communication channel. Here is a conceptual Python script that could be scheduled via cron:
#!/usr/bin/env python3
import subprocess
import json
from datetime import datetime
# Query git for all RFC files changed in the last 7 days
result = subprocess.run(
["git", "log", "--since=7.days.ago", "--name-only", "--diff-filter=A", "--format=%H"],
capture_output=True, text=True
)
hashes = result.stdout.strip().split("\n")
status_counts = {"draft": 0, "proposal": 0, "under review": 0, "approved": 0, "implemented": 0, "closed": 0}
for commit_hash in hashes:
# Get files changed in this commit
files_result = subprocess.run(
["git", "diff-tree", "--no-commit-id", "--name-only", "-r", commit_hash],
capture_output=True, text=True
)
files = files_result.stdout.strip().split("\n")
for f in files:
if not f.startswith("rfcs/"):
continue
# Read the status field
status_match = subprocess.run(
["grep", "-oP", "status: \\K\\S+", f],
capture_output=True, text=True
)
if status_match.stdout.strip() in status_counts:
status_counts[status_match.stdout.strip()] += 1
now = datetime.utcnow().isoformat() + "Z"
print(f"[{now}] RFC status counts: {json.dumps(status_counts)}")
This script is not tied to any specific product; it reads git history, extracts a front-matter field, and emits a JSON payload. Your team can pipe this output into a Slack webhook, a Teams connector, or a dashboard panel. The pattern—query version control, parse a known field, emit a summary—is the concrete takeaway.
If your organization uses a dedicated issue tracker (Jira, GitLab Issues, Bugzilla), the same pattern applies: query for RFC-type tickets, read a custom field for status, and surface counts. The mechanism is always the same: an API or CLI query, a field extraction, and a presentation layer.
A silent failure in status tracking occurs when the status field diverges from the actual workflow state. An author may set status: approved but never update it to implemented, leaving the RFC in a permanent “approved” state indefinitely. This is especially common when the status field is treated as a one-time fill-in rather than a living attribute. Mitigate this by tying status transitions to concrete events—e.g., moving from approved to implemented only occurs when a related pull request is merged, or when a deployment event is recorded.
The most prevalent failure is an RFC that never progresses beyond under review. This happens when reviewers are not assigned, when the Decision Needed section is vague, or when the transition guard blocks movement because the authorized actor is unavailable. The cure is to design explicit escalation paths: if an RFC is stagnant for X weeks, automatically reassign reviewers or notify a process owner.
status and prioritySome teams add a priority field alongside status and treat them as interchangeable. They are not. Status tracks the lifecycle state; priority (if used) tracks urgency relative to other work. Mixing the two leads to RFCs that are marked high priority but remain in draft status, or low priority RFCs that are approved but never implemented. Keep them separate, and if your tooling does not support a priority field, do not invent one—use labels or tags instead.
Over time, authors begin omitting sections they perceive as “not relevant to me,” even though those sections contain information reviewers need. This drift is silent because the RFC still submits and moves through the process, but the missing data forces reviewers to ask follow-up questions, adding friction. Combat template drift by making omitted sections non-blocking but documented as “required for full review.” If a section is consistently left empty, consider whether it is genuinely needed and, if so, adjust the template or the process.
When the same few individuals are assigned every RFC, they eventually stop reading thoroughly. The process degrades into a checkbox exercise. Rotate reviewers systematically, or implement a “read-and-return” model where reviewers signal they have skimmed the RFC without approving, allowing the author to proceed or seek additional eyes.
As noted, the status field can become out of sync with reality. A RFC may be approved in the database but draft in the pull request, or vice versa. The sharp edge is that no automated check catches this unless you explicitly compare the two sources. Design your process so that status changes are tied to version control events—merge, close, deploy—rather than free-form edits.
The patterns above are not tied to any product’s native features. If your team uses GitHub, you can implement the status field as a label, the transition guard as branch protection, and the reviewer assignment as CODEOWNERS or MR description parsing. If you use GitLab, the same patterns map to labels, merge request rules, and reviewers in the MR description. If you use a homegrown wiki or issue tracker, the patterns map to custom fields, API queries, and scheduled scripts.
What remains constant is the shape of the problem: defining states, designing templates, assigning reviewers, tracking status, and recognizing the failure modes that arise when those pieces are misaligned or poorly maintained. Write your process design around that shape, and you will have an RFC process that survives contact with real change requests.
This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy