Every conversation about autonomous software in a school arrives at the same question, usually within the first two minutes: what happens when it gets it wrong?
The honest answer is not “it won’t”. The honest answer is architectural — you decide, per action type, which category of mistake you are willing to absorb silently and which you want to see before it happens. The approvals inbox is where the second category lands.
Autonomy With a Stop Button
The design principle is narrow permission by default. When an automation runs in agent mode, it may propose any number of actions. It may execute only the action types that appear in two lists at once: the pack’s own declaration of what it is allowed to do directly, and a platform-level list of action types that are ever eligible for direct execution.
In practice this means a fee automation can send an approved reminder template on its own, because that is what it exists to do and the blast radius of a wrongly-toned reminder is a mildly annoyed parent. The same automation cannot waive a fee, cancel a ledger entry, or adjust a balance, even if the reasoning is sound and the amount is small. Those get proposed.
Anything outside the intersection is not refused — it is deferred. The action, its full payload, and the reasoning behind it are written to a queue and wait for a human.
What Lands in the Proposal Queue
Three routes put a row in the queue.
Action type outside the allow-list. The most common case. An automation reasoned its way to something structurally consequential and the engine redirected it.
Dry-run mode. When a pack is running in preview, every action it would have taken is queued instead of dispatched. This is the intended way to evaluate a new pack: turn it on in dry run, let it accumulate a week of proposals, and read what it would have done to your school before letting it do anything.
Allow-list explicitly emptied. A school that wants a pack’s output but not its autonomy can reduce its permitted set to nothing, which routes every proposed action through review.
Actions that execute directly never appear here. That is a real gap in visibility, and the answer to it is the firing log rather than this queue — every dispatch writes a row there whether or not a human touched it.
Anatomy of a Proposal Row
A proposal carries more than the action itself, because “approve or reject” is an impossible question without context.
The action type and its full payload. Exactly what will be dispatched, including the template and the resolved variables.
The reasoning. A short written justification captured at the moment the proposal was made — why this student, why this tone, why now.
The affected count. How many people or records this action touches. The difference between one and two hundred is the difference between a quick yes and a careful read.
The estimated cost in paise. What dispatching this will actually cost in messaging charges. Small per row; not small across a term.
Provenance. Which pack proposed it, which rule, when, and a trace identifier linking back to the reasoning run.
An expiry timestamp. Every proposal has a deadline.
That last field is the one that makes the design safe. A queue whose rows sit indefinitely eventually gets cleared in bulk by someone in a hurry, and bulk approval of stale proposals is exactly the failure mode this queue exists to prevent.
Approve, Reject, or Let It Expire
Three outcomes, and they behave differently.
Approve dispatches the stored payload immediately through the same action executor a scheduled rule would use — meaning the same deduplication check applies. Approving a reminder that has already gone out through another route results in a duplicate skip, not a second message. The proposal is stamped with your user and the review time.
Reject sets the status and dispatches nothing. The row and its reasoning remain readable, which matters more than it sounds: a pack that accumulates rejections is a pack whose thresholds are wrong, and the rejection history is the evidence for that argument.
Expire is what happens by default. No dispatch, no stamp, no action. Inaction is the safe outcome, deliberately.
A proposal that is no longer pending cannot be approved twice — a second attempt is refused rather than silently repeated.
The Separate Human Approvals Queue
It is worth separating two things that both get called “approvals”.
The proposal queue above is about machine-originated actions awaiting human sign-off. There is a second, older queue that handles human-originated requests awaiting senior sign-off: fee concessions, one-time fees, admissions, and payment reversals. That queue is gated on its own permission, held to admin-level access, and every one of its request types is financial.
Keeping them apart is intentional. The questions are different. Reviewing a proposal asks “should the system have done this?” Reviewing an approval request asks “should this person be allowed to do this?” Merging them into one inbox would blur the second question into the first, and the second question is the one auditors ask about.
Designing a Review Habit That Scales
A queue is only a control if someone reads it. Three practices make that sustainable.
Give it an owner and a slot. One named person, once a day, at a fixed time. Not “the office will check it.”
Treat volume as a signal. A queue that grows steadily means a pack is proposing things it should not be proposing, or that its thresholds are miscalibrated. The fix is upstream in the pack settings, not downstream in faster clicking.
Let expiry work. If a proposal expired and nothing bad happened, that action was not worth proposing. Note it, tighten the pack, move on. Retroactively approving expired rows defeats the purpose of the deadline.
The review queue is one of three guardrails; the other two — deduplication and circuit breakers — are described in school autopilot: operations that run without you.