Human-in-the-Loop for AI in SaaS Operations
Human-in-the-loop for AI in SaaS operations means gating tenant-level admin actions — plan overrides, quota bumps, account suspensions — behind a human tap before an ops agent touches a customer's account.
Ops agents have root, not just an opinion
A support or growth agent that triages tickets and drafts replies is annoying when wrong. An ops agent wired into your admin panel is dangerous when wrong, because the actions it can take are structural: extend a trial, raise a rate limit, change a billing plan, suspend or delete a tenant. These aren't content a customer reads and can shrug off — they're state changes to a paying account, and some of them (deleting a tenant, dropping a plan tier mid-cycle) aren't cleanly reversible.
The pattern that works isn't "trust the agent less" — it's "make the handful of high-blast-radius calls unreachable without an approved decision first." Everything else the agent already does unattended keeps running unattended.
Sorting ops actions by blast radius
| Action | Reversible? | Gate it? |
|---|---|---|
| Answer "what plan am I on" from account data | n/a | No |
| Apply a pre-approved dunning email retry | Yes | No |
| Extend a trial by N days | Yes, easily | No, if capped (e.g. ≤14 days) |
| Override a plan's seat or rate limit | Yes, but changes billing | Yes |
| Suspend a tenant for abuse/non-payment | Yes | Yes |
| Delete a tenant or purge its data | No | Yes |
The bottom three go through Impri as pending actions. A capped, self-service trial extension can stay automated — the moment "N days" is unbounded or touches an invoice, it moves up a row.
Gating a plan override from a TypeScript ops bot
The agent still decides what change is warranted; Impri sits between that decision and the admin API call that makes it real.
import { setTimeout as sleep } from "node:timers/promises";
const API = "https://api.impri.dev/v1";
const HEADERS = {
Authorization: `Bearer ${process.env.IMPRI_API_KEY}`,
"Content-Type": "application/json",
};
async function proposePlanOverride(
tenantId: string,
field: "seat_limit" | "rate_limit_rpm",
from: number,
to: number,
reason: string,
) {
const res = await fetch(`${API}/actions`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
kind: "tenant.plan_override",
title: `Override ${field} for tenant ${tenantId}: ${from} → ${to}`,
preview: {
format: "markdown",
body: `**Tenant:** ${tenantId}\n**Field:** ${field}\n**From → To:** ${from} → ${to}\n**Reason:** ${reason}`,
},
editable: ["preview.body"],
idempotent: false, // re-applying the same override could double-count usage credits
undo: `PATCH tenant ${tenantId} ${field} back to ${from} via admin API`,
expires_in: 21600, // 6h — ops requests go stale once the underlying ticket is resolved elsewhere
}),
});
const { id } = await res.json();
let decision;
while (true) {
const poll = await fetch(`${API}/actions/${id}`, { headers: HEADERS });
decision = await poll.json();
if (decision.status !== "pending") break;
await sleep(5000);
}
if (decision.status !== "approved") return null; // rejected or expired — nothing changes
const finalBody = decision.decision.final_preview.body;
await applyPlanOverrideInAdmin(tenantId, field, to); // your internal admin API call
await fetch(`${API}/actions/${id}/result`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({ status: "executed", payload: { tenantId, field, to } }),
});
return finalBody;
}Tenant suspension and deletion use the same three-call shape with kind: "tenant.suspend" or "tenant.delete" — the preview should show current MRR, seat count, and last-login so whoever's on call isn't approving a delete blind.
Why the undo field matters more here than anywhere else
Most ops mistakes are fixable if someone knows how fast. A wrong plan override is a two-minute PATCH; a wrong tenant deletion, without a backup step described somewhere, is not. Writing the undo string at proposal time forces the agent's author to have already thought through reversibility before the human is asked to trust the change — see handling edits and the undo field for the full field reference.
What Impri does not do here
Impri stores the proposed override or suspension, notifies whoever's on ops duty, and holds the decision — it doesn't know your pricing rules, doesn't call your admin API, and doesn't decide whether a 3x rate-limit bump for a customer is generous or reasonable. That judgment stays with the human, and applyPlanOverrideInAdmin is still your code to write and to wrap so it's unreachable without the approved decision. Every approved and rejected override is still worth checking against the audit log later, especially for tenants that file a dispute months after a plan change.
Next: approve database writes from an AI agent if your ops agent is closer to the data layer than the admin panel, and the propose-approve-execute pattern for the underlying design this is built on.