Abstract: Most risk management frameworks are written for organizations with a PMO, a risk officer, and quarterly steering committees. A two- to five-person dev shop has none of that, and doesn’t need it. This post shows a stripped-down risk register and scoring scheme I’ve used on client projects for the last several years — enough structure to catch problems early, not enough to become a job in itself.
Why bother with a formal framework at all
I resisted this for years. On a small team you talk every day, you know what’s on fire, why write it down? The answer I eventually landed on: memory is not a risk log. A risk you discussed once in a stand-up two months ago and never revisited doesn’t get mitigated, it gets forgotten until it becomes an incident. A written register, even a tiny one, forces you to revisit open risks on a schedule instead of relying on someone happening to remember.
The other reason is client communication. When a risk turns into a missed deadline, “we knew about this in March and tracked it” is a very different conversation than “this caught us off guard.” I’ve been on both sides of that conversation. The first one is much shorter.
What full frameworks get wrong for small teams
PMI’s risk management process, ISO 31000, and most enterprise risk registers assume a risk owner who isn’t also the person writing code. They assume a review cadence run by someone other than the four people doing the work. And they assume enough risks to justify categorization taxonomies — technical, financial, schedule, vendor, compliance, and so on, each with its own workflow.
On a small team, the person who identifies the risk, owns it, and mitigates it is usually the same person, and there might be six open risks total. A framework with fifteen fields per entry gets abandoned within a month. I’ve watched this happen on two different client engagements before I stripped it down.
A minimal risk register
The register itself doesn’t need a tool. A single table works, whether it lives in a spreadsheet, a Markdown file in the repo, or a database table if you want it queryable. Here’s the schema I use when it lives in MSSQL, since that’s usually already sitting on the project:
CREATE TABLE dbo.RiskRegister (
RiskId INT IDENTITY(1,1) PRIMARY KEY,
Title NVARCHAR(200) NOT NULL,
Description NVARCHAR(MAX) NULL,
Probability TINYINT NOT NULL CHECK (Probability BETWEEN 1 AND 5),
Impact TINYINT NOT NULL CHECK (Impact BETWEEN 1 AND 5),
Score AS (CAST(Probability AS INT) * CAST(Impact AS INT)) PERSISTED,
Owner NVARCHAR(100) NOT NULL,
MitigationPlan NVARCHAR(MAX) NULL,
Status NVARCHAR(20) NOT NULL DEFAULT 'Open',
RaisedOn DATE NOT NULL DEFAULT CAST(GETDATE() AS DATE),
LastReviewed DATE NULL,
CONSTRAINT CHK_Status CHECK (Status IN ('Open', 'Mitigated', 'Accepted', 'Closed'))
);
Nine fields. Score is a computed column so you never have to remember to update it when probability or impact changes. Status has exactly four values because I’ve found more than that just creates ambiguity about what “in progress” means for a risk versus a task.
If you’d rather keep it out of a database entirely, the same structure works as a flat file. I use this format when the risk register lives in the repo next to the code, which has the advantage of being versioned alongside the project itself:
# risks.yaml
- id: 1
title: "Third-party payment API has no sandbox for our region"
probability: 4
impact: 5
owner: andriy
status: open
mitigation: "Build against mocked responses; schedule live test window with vendor before go-live"
raised: 2026-06-01
last_reviewed: 2026-08-15
Scoring without overthinking it
Probability and impact each get a score from 1 to 5, multiplied together for a range of 1 to 25. I don’t bother with more granular scales — a 1-to-10 scale invites false precision, since nobody can actually distinguish a probability of 6 from a probability of 7. Three buckets is enough to drive action:
A score of 1 to 6 means monitor it and move on, no action needed beyond noting it exists. A score of 8 to 12 means it needs a mitigation plan written down, even a rough one. A score of 15 or above means it gets discussed before the next sprint starts, full stop, regardless of what else is on the agenda.
Here’s a small script I run against the YAML file to flag anything that’s crossed the review threshold or gone stale — useful if the register lives in the repo and isn’t getting a dedicated meeting:
#!/usr/bin/env python3
"""Flag high-score or stale risks in risks.yaml. Run this weekly, e.g. from a git hook or cron."""
import yaml
import sys
from datetime import date, timedelta
REVIEW_INTERVAL_DAYS = 14
HIGH_SCORE_THRESHOLD = 15
def load_risks(path="risks.yaml"):
with open(path) as f:
return yaml.safe_load(f)
def check_risks(risks):
today = date.today()
flagged = []
for risk in risks:
if risk["status"] not in ("open",):
continue
score = risk["probability"] * risk["impact"]
last_reviewed = risk.get("last_reviewed", risk["raised"])
days_stale = (today - last_reviewed).days if isinstance(last_reviewed, date) else 999
reasons = []
if score >= HIGH_SCORE_THRESHOLD:
reasons.append(f"high score ({score})")
if days_stale > REVIEW_INTERVAL_DAYS:
reasons.append(f"not reviewed in {days_stale} days")
if reasons:
flagged.append((risk["title"], reasons))
return flagged
if __name__ == "__main__":
risks = load_risks()
flagged = check_risks(risks)
if not flagged:
print("No risks need attention.")
sys.exit(0)
print("Risks needing attention:")
for title, reasons in flagged:
print(f" - {title}: {', '.join(reasons)}")
This isn’t sophisticated, and it doesn’t need to be. It just makes sure a risk that’s been sitting untouched for three weeks gets surfaced instead of silently rotting.
The review cadence that actually survives
Weekly, ten minutes, tied to something you’re already doing — I attach it to the sprint planning meeting rather than creating a separate calendar invite, since a standalone “risk review” meeting is the first thing that gets skipped when the week gets busy. The agenda is short: any new risks since last time, any risk that’s crossed a score threshold, and a one-line status update on anything already marked as being mitigated.
I explicitly don’t review low-score risks in detail every week. They sit in the register, they get glanced at, and unless something’s changed they get left alone. Reviewing everything in depth every time is exactly the overhead that gets a framework abandoned.
Where this breaks down
This approach doesn’t scale past maybe eight or ten active risks before the flat scoring starts hiding real differences — a score-20 risk about a vendor contract and a score-20 risk about a flaky test suite are not the same kind of problem, and at some point you do need categories to make sense of the list. It also assumes the team is small enough that “owner” can just be a name rather than a role, which breaks down once you have more than one team lead. If you’re past that point, this is a starting template to adapt, not a ceiling.
It also doesn’t replace incident response. A risk register is for things you can see coming. When something blindsides you, that’s a postmortem, not a register entry — though it’s worth adding a line to the register afterward for “this category of thing can happen again.”
Conclusion
A risk register only works if it’s cheaper to maintain than the cost of the risks it catches. Nine fields, three score buckets, and a ten-minute weekly check tied to an existing meeting is deliberately close to the minimum that still counts as “tracking risk” rather than “hoping.” Start here, and only add fields or process when you can point to a specific risk that slipped through because the current setup was too thin.
FAQ
Do I need special software for this? No. A spreadsheet, a YAML file in the repo, or a small database table all work equally well. Pick whatever’s already part of your workflow so the register doesn’t become one more tool to remember to open.
What if the client wants a “proper” risk management document for compliance reasons? Export the same data into whatever template they require. The underlying register doesn’t need to look like the deliverable; it just needs to contain the same information.
How is this different from a bug tracker or a task board? A risk is something that hasn’t happened yet and might not happen at all — it’s about uncertainty, not confirmed work. Mixing risks into the same board as tasks tends to bury them under things that are certain and already actionable.
Should probability and impact be scored by one person or the whole team? Whoever raises the risk proposes a score, and it gets a quick sanity check in the weekly review. I’ve found requiring group consensus on every score just slows the register down without meaningfully improving the numbers.
What happens to a risk that never materializes? Close it, with a one-line note on why. Don’t delete it — a closed risk that never happened is useful history for scoring similar risks on the next project.





Leave a Reply