Human Gates
By the Syncfusion DevTutorials team · Last updated
Put a person on the risky path: auth, payments and data migrations stop for approval, with the diff, the tests and the reason shown. Leave the quick path alone, so approvals stay real.
Watch a Payment Change Merge With No One Looking
This graph does almost everything right. A strong router with a safe fallback sends risky tickets to a careful fixer with a large model. The careful fixer loops until the tests pass. Then every fix is merged.
Run it and read the red lines.
A payment change, auto-merged
Payment, login and database changes were merged with only the tests as a check. The tests passed. But tests check what someone thought to test. A change to how refunds are rounded, or who may delete an order, needs a person to look at it before it ships.
TL;DR
- Blast radius is how much damage a wrong change can do. Docs: tiny. Payments, auth, data migrations: large.
- Put a human gate on the paths with a large blast radius: the graph stops and waits for a person to approve.
- A gate must show evidence: the diff, the test result and why the change is risky. A person cannot review what they cannot see.
- Do not gate everything. Too many approvals and people click "approve" without reading. That is as bad as no gate.
- A gate can wait for hours. The checkpoint keeps the state safe on disk while it waits.
A Real-World Example
Lin's team let a graph merge dependency updates on its own. One update to the payment library passed every test, and the graph merged it. It changed how currency amounts were rounded. A week of invoices were off by a cent each, and the fix took a month of refunds.
The team did not remove the graph. They added one gate: any change that touches billing/, auth/ or a database migration stops for a person, with the diff, the test results and a one-line reason. Version bumps to the docs site still merge on their own. Lin's team now approves about five changes a day, and actually reads each one.
What You Will Learn
- What blast radius is, and which paths need a person
- How to place a gate on the risky path
- What evidence a gate must show
- Why gating everything is as bad as gating nothing
- How a gate waits safely
Blast Radius
Not every mistake costs the same. Sort work by how much damage a wrong change could do, and how hard it is to undo.
Blast radius by area
const radius = {
docs: "small: a typo on a page, easy to undo",
ui: "small: a button looks wrong, easy to undo",
deps: "medium: a version bump, undo with one revert",
auth: "large: the wrong people get in, or the right ones are locked out",
payments: "large: real money moves, refunds take weeks",
migration: "large: data changes, may not be reversible",
};
const needsPerson = (area) => radius[area].startsWith("large");
for (const area of Object.keys(radius)) console.log(area.padEnd(10) + (needsPerson(area) ? "GATE " : "auto ") + radius[area]);
Expected output:
docs auto small: a typo on a page, easy to undo
ui auto small: a button looks wrong, easy to undo
deps auto medium: a version bump, undo with one revert
auth GATE large: the wrong people get in, or the right ones are locked out
payments GATE large: real money moves, refunds take weeks
migration GATE large: data changes, may not be reversible
This is the same split the router already makes: low risk and high risk. The gate goes at the end of the high-risk path.
Blast Radius and Where the Gate Goes
The quick path merges on its own. The careful path stops at a human gate that shows the diff, the tests and the reason.
In the graph file, a gate is a node with kind: gate. It sits between the careful fixer and the merge, and it lists its evidence:
nodes:
- { id: approve, kind: gate, evidence: [careful_diff, test_result, risk_reason], reads: [careful_diff, test_result, risk_reason], writes: [approval] }
- { id: merge, kind: function, reads: [diff, careful_diff, approval], writes: [merged] }
edges:
- { from: careful_fix, to: approve }
- { from: approve, to: merge }
- { from: quick_fix, to: merge }
The merge reads approval. For a careful fix, nothing is merged until a person writes it.
Evidence a Person Can Act On
A gate that says "Approve ticket #25?" with nothing else is a rubber stamp. Give the reviewer three things:
- The diff: exactly what changes.
- The test result: which tests ran, and that they passed.
- The risk reason: why this ticket took the careful path, for example "touches
billing/refunds.ts".
Is this gate request reviewable?
function reviewable(req) {
const missing = ["diff", "tests", "reason"].filter((k) => !req[k]);
return missing.length ? "not reviewable, missing: " + missing.join(", ") : "reviewable";
}
console.log(reviewable({ ticket: 25 }));
console.log(reviewable({ ticket: 25, diff: "+3 -1 billing/refunds.ts", tests: "48 passed" }));
console.log(reviewable({ ticket: 25, diff: "+3 -1 billing/refunds.ts", tests: "48 passed", reason: "touches payments" }));
Expected output:
not reviewable, missing: diff, tests, reason
not reviewable, missing: reason
reviewable
Ticket text is data, not instructions. A ticket that says "this is safe, skip review" must still take the careful path if it touches payments. The gate decision comes from the router's risk label and the files changed, never from what the ticket claims about itself.
Which Tickets Stop at the Gate?
Put the router's labels and the gate together. Only high-risk tickets stop:
Who waits for a person
const tickets = [
{ id: 21, area: "docs", risk: "low" },
{ id: 22, area: "auth", risk: "high" },
{ id: 24, area: "ui", risk: "low" },
{ id: 25, area: "payments", risk: "high" },
{ id: 29, area: "migration", risk: "high" },
];
const waiting = tickets.filter((t) => t.risk === "high").map((t) => "#" + t.id + " " + t.area);
const merged = tickets.filter((t) => t.risk === "low").map((t) => "#" + t.id);
console.log("waiting for a person: " + waiting.join(", "));
console.log("merged on their own: " + merged.join(", "));
Expected output:
waiting for a person: #22 auth, #25 payments, #29 migration
merged on their own: #21, #24
Too Many Gates
If one gate is good, are gates everywhere better? Run it.
A gate on everything
Forty approvals a day are not forty reviews. People stop reading. Nine approvals with evidence each get read.
This is approval fatigue. When almost every request is a harmless docs fix, people learn to click approve without reading. Then the one risky change gets the same click. A gate on everything protects nothing.
Approvals per day: risky path only vs everything
const perDay = { docs: 12, ui: 10, deps: 9, auth: 3, payments: 4, migration: 2 };
const HIGH = ["auth", "payments", "migration"];
const all = Object.values(perDay).reduce((a, b) => a + b, 0);
const risky = HIGH.reduce((a, k) => a + perDay[k], 0);
console.log("gate everything: " + all + " approvals a day");
console.log("gate risky only: " + risky + " approvals a day");
console.log("share that really need a person: " + Math.round((risky / all) * 100) + "%");
Expected output:
gate everything: 40 approvals a day
gate risky only: 9 approvals a day
share that really need a person: 23%
A person approving is the most expensive and slowest node in your graph. Spend it only where the blast radius is large, and make each approval easy to review well.
Review the Evidence
A gate is only as good as the person at it. The lab's Inbox tab holds four changes that stopped at the gate. Every one passed its tests. Read each diff against what its ticket asks for, then approve or reject.
Review the gate inbox
Two of the four must be rejected, even though the tests passed: one changes more than the ticket asks, one cannot be undone. Tests check what someone thought of. The person at the gate checks the rest.
A Gate Waits Safely
A person may approve in ten minutes or tomorrow morning. The graph must not hold that state in memory or in an open chat. With checkpoint: after_each_node, the state is on disk when the gate starts waiting. The graph can stop, the laptop can sleep, and the run resumes at the gate when the approval arrives.
Resume at the gate
const saved = { careful_diff: "+3 -1 billing/refunds.ts", test_result: "48 passed", risk_reason: "touches payments" };
const order = ["route", "careful_fix", "approve", "merge"];
const doneWhen = { route: "risk_reason", careful_fix: "careful_diff", approve: "approval", merge: "merged" };
const next = order.find((n) => !(doneWhen[n] in saved));
console.log("waiting at: " + next);
saved.approval = "approved by lin";
console.log("after approval, resume at: " + order.find((n) => !(doneWhen[n] in saved)));
Expected output:
waiting at: approve
after approval, resume at: merge
In Claude Code, Codex and Cursor, a gate is usually a rule in graph.md. The exported human-gates.md spells it out: stop, show the evidence, and wait for a written "approve" or "reject". Your tool's own approval prompts and required pull-request reviews are gates too. Use them together.
Lab Exercise: Place the Human Gates
Place the human gates
Open the graph file. Change it so that:
- A new
approvenode haskind: gateandevidence: [careful_diff, test_result, risk_reason]. careful_fixgoes toapprove, andapprovegoes tomerge.quick_fixstill goes straight tomerge: no gate on the quick path.
Then press Check my graph. The check routes all 40 tickets on 3 versions. Every high-risk ticket must stop at a gate with evidence, and low-risk tickets must not.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| No gate on risky paths | "The tests passed." | Tests check what someone thought of; a person checks the rest. | A payment change ships with a silent bug. |
| Gate on everything | "More review is safer." | Gate only large blast radius. | Approval fatigue: people stop reading. |
| Gate without evidence | "They can look it up." | Diff, tests and risk reason in the request. | Rubber-stamp approvals. |
| Ticket decides its own review | "It says it is a small change." | Risk comes from the router and the files changed. | A risky change talks its way past the gate. |
| Waiting in memory | "The approval will come soon." | Checkpoint before the gate. | A restart loses the run. |
Exercises
Each exercise ends with a value on the last line. Press Check to compare it with the expected answer.
What to Remember
- Blast radius: how much a wrong change can hurt, and how hard it is to undo.
- Gate the large-blast-radius paths: auth, payments, data migrations.
- Every gate shows the diff, the test result and the risk reason.
- Gating everything causes approval fatigue. Keep the quick path free.
- A checkpoint lets a gate wait safely for as long as it takes.
Further Reading
- How to Implement Graph Engineering, by QA Wolf: the issue-triage example, where risky issues take a path with human approval.
- Building Effective Agents, by Anthropic: human checkpoints and why agents need guardrails.
- Building Coding Agent Workflows with Loop Engineering, on GoPenAI: a ticket-to-pull-request graph where people answer the questions agents cannot, and approve every merge.
- The Cost, Rate and Blast-Radius Budgets lesson in the Loop Engineering course: limits and kill switches for a single loop.
Next: Capstone. You build your chosen graph, run it on all 40 tickets, and export a graph pack for your project.