AI coding

Reading the Proposal: A Queue for Agent Writes Before Apply

Antonio Evans8 min read

An agent submits a proposal card listing target rows, expected effect and reversal cost to a human reviewer seated at a desk, who approves it and sends it to a database icon labeled Apply.
Key points

Key points

  1. 1A write proposal is a structured record an agent submits before touching data: target rows, expected effect, reversal cost, and the exact operation it intends to run.
  2. 2Routing is a function of reversal cost and data sensitivity, not of which team happens to be online, so the approver is decided by the proposal's content.
  3. 3The decision log is the contract: the apply step reads it and refuses to run unless a valid approval exists for that exact proposal hash.
  4. 4Approval must be bound to a payload hash and a freshness check, because the reviewed state can change while a person is deciding.

The problem a queue actually solves

An agent that reads and then writes follows a loop: plan, call a tool, observe the result, plan again. When a tool call times out after the server already committed it, a persistent agent often retries, and you get two invoices, two refunds, or two ownership changes. That pattern is well documented in the failure analysis of side-effecting tool calls, where the recommendation is that every side-effecting call carry an idempotency key and store the outcome server side the tool calling analysis. A write proposal queue generalizes that idea: the proposal id becomes the idempotency key, and the approval is the thing that makes the retry safe.

The second problem is reviewability. A plan that says "clean up the CRM" is too broad to approve. A preview that names the records, the current values, the proposed values, and the downstream effects gives a person a concrete decision, and execution should then be constrained to exactly that reviewed proposal the preview playbook. The queue is where that constraint lives.

The third problem is auditability. When an auditor asks who changed a record and why, an approval record with a named human, a timestamp, and the proposal body answers the question. Approval gates reduce the impact of mistaken tool calls by inserting verification before side effects occur, and they support separation of duties and documented decision making the HITL workflow guide.

The proposal record: the six fields that matter

A proposal is a row in its own table, not a message in a chat log. Minimum viable schema:

FieldWhy it existsExample
proposal_idIdempotency key and join key for the decision logwr_01J8...
payload_hashBinds approval to the exact operation reviewedSHA-256 of canonical JSON
target_rowsThe concrete set of rows, with current values412 leads, IDs listed
expected_effectWhat will differ after apply, plus known side effectsowner field changes, email queued
reversal_costHow the change is undone and what it costsre-run inverse update, no notification suppress
requester_contextAgent identity, task id, model, source promptagent-crm-07, task T-118

Two of these deserve emphasis. target_rows must be a resolved list or a bounded query with a count, not a predicate like WHERE territory = 'west', because the row set changes between review and apply. expected_effect must include side effects that the reviewer would not infer from the field name: a notification sent, a work queue reassigned, a downstream automation triggered. The preview guidance is explicit that a reviewer who approves a field change should not discover afterwards that it also contacted a customer the preview playbook.

Here is a compact shape you can store as JSONB:

{
  "proposal_id": "wr_01J8ZK4Q",
  "payload_hash": "sha256:9f2c...",
  "operation": "UPDATE leads SET owner_id = $1 WHERE id = ANY($2)",
  "target_rows": [
    {"id": "A-104", "owner_id": "u_alex", "open": true, "exception": null},
    {"id": "A-118", "owner_id": "u_alex", "open": true, "appointment": "2026-11-02"}
  ],
  "row_count": 2,
  "expected_effect": "owner_id changes on 2 rows; 1 appointment handoff email queued",
  "reversal": {"strategy": "inverse_update", "estimated_minutes": 6, "notification_reversible": false},
  "reversal_cost": "low",
  "requester": {"agent": "agent-crm-07", "task": "T-118", "source": "territory_reassign"}
}

The reversal_cost field is the routing signal. Treat it as an enum with a written justification, not a free number, because a number invites false precision and an enum forces the agent to commit to a category.

Classifying reversal cost with a decision rule

Most teams get stuck here because they try to score risk with a formula. A simpler rule works: classify by what it takes to undo, then escalate on sensitivity separately.

Reversal classDefinitionTypical examplesDefault route
R1 trivialInverse operation is a single statement, no external effectinternal ticket status, draft record fieldAuto-approve with sampling
R2 moderateInverse exists but must be run by a human or script, no external effect yetowner reassignment, batch attribute updateTeam lead review
R3 hardExternal effect already possible or history overwrittensent email, refund, published contentService owner plus QA
R4 irreversibleCannot be undone without compensation or data lossrow deletion, credential rotation, wireDual approval, change window

This tracks the risk-based rule that approval is required when the next action is irreversible, costly, regulated, or high blast radius, and that writes to production databases belong with a service owner or on-call engineer using a proposed diff, preconditions, a rollback plan, and an idempotency key the HITL workflow guide. Note that the guide lists writes to production databases as high cost of error with medium reversibility, which is exactly the R3 bucket.

Sensitivity is a second axis and should be checked independently. A change to a table holding personal data is not R1 even if the SQL inverse is trivial, because the exposure itself is not undone by an inverse update.

Routing: deciding who approves before the agent asks

Route on three inputs: reversal class, affected table or domain, and time of day for SLA escalation. Do not route on the agent's own confidence, because a confident agent is not a calibrated one.

def route(proposal, table_policy):
    policy = table_policy[proposal.table]
    if proposal.reversal == "R4":
        return [policy.owner, policy.second_approver], "PT4H"
    if proposal.reversal == "R3":
        return [policy.service_owner], "PT2H"
    if proposal.reversal == "R2":
        return [policy.team_lead], "PT8H"
    return [], "auto"          # R1 plus sampling

Keep the policy table in version control next to the schema. When a new table appears, the absence of an entry should fail closed and route to R3 until someone writes a policy, not silently auto-approve.

Two operational details make routing fast rather than bureaucratic. First, an evidence pack: the proposal, the diff, the preconditions, and the rollback plan on one screen, so a decision takes seconds rather than a meeting. Second, approve-with-edits, so a reviewer who wants a smaller scope does not force the agent to restart the whole plan the HITL workflow guide. If the reviewer edits the payload, recompute the hash and re-route if the class changed.

The decision log as a contract

The apply step should not trust the queue's current state. It should read an append-only decision log and require a match. Five conditions, all mandatory:

  1. A decision row exists for this proposal_id.
  2. Its payload_hash equals the hash of the operation about to run.
  3. The decision is approved, not pending, rejected, or expired.
  4. The approver's role matches the route computed at submit time.
  5. The decision is within its validity window and the target rows still match the reviewed state.

A decision record worth writing:

{
  "proposal_id": "wr_01J8ZK4Q",
  "payload_hash": "sha256:9f2c...",
  "decision": "approved",
  "approver": {"id": "u_sam", "role": "team_lead", "auth": "sso"},
  "decided_at": "2026-10-14T09:12:04Z",
  "valid_until": "2026-10-14T10:12:04Z",
  "edits": null,
  "reason": "appointment handoff confirmed with ops"
}

Log the decision in the same database as the data being changed, inside the same transaction that performs the apply, or at minimum with a foreign key. If your audit requirements mean the log also has to survive outside the application, database-level auditing is the backstop: the pgAudit extension extends native PostgreSQL logging to capture the user who made a change, and it redacts sensitive values such as cleartext passwords from the logs the AWS pgAudit guide. Use both: an application decision log for the business reason, and statement-level auditing for the independent record of what the SQL actually did.

Expiry matters more than teams expect. A preview can go stale while a person is deciding, so the apply step has to recheck the current state before writing the preview playbook. Concretely: re-read the target rows, compare their version column or a hash of the reviewed fields, and abort with a stale_proposal error if anything moved.

Enforcing the gate in the database, not just the code

An application-level queue is a promise. A database-level grant is an enforcement. The cheapest way to make the gate real is to give the apply path its own role with only the privileges it needs, and to make sure the agent's own credentials cannot write at all.

Role design follows a least-privilege pattern: create roles per use case such as readonly and readwrite, grant them only the object privileges required, and keep the master or admin user out of application code the AWS roles guide. Postgres privileges are granted per object, and a role that has only CONNECT can log in but do nothing until it receives USAGE on the schema and SELECT on the tables the read-only role discussion. That is the shape you want for the agent.

-- agent: can see what it must propose against, cannot write
GRANT CONNECT ON DATABASE app TO agent_readonly;
GRANT USAGE ON SCHEMA public TO agent_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO agent_readonly;

-- apply service: the only writer
GRANT SELECT, UPDATE ON leads TO writer_apply;

-- proposals and decisions: append only for the app role
GRANT SELECT, INSERT ON write_proposals TO writer_apply;
GRANT SELECT, INSERT ON write_decisions TO writer_apply;
REVOKE UPDATE, DELETE ON write_decisions FROM writer_apply;

The GRANT syntax and the privilege list (SELECT, INSERT, UPDATE, DELETE, TRUNCATE, and others, or ALL) are documented in the PostgreSQL reference the GRANT documentation. The append-only revoke is the part that makes the decision log trustworthy: the same role that applies changes cannot rewrite history.

For a worked cost comparison, consider two designs for the same 2-row update. In the direct-write design, a duplicate retry costs one wasted write and a manual correction, plus the reviewer time to find it. In the proposal design, a duplicate retry hits the unique constraint on proposal_id in the decision log and returns the original outcome. The design costs one extra table, one extra insert on approve, and a policy file. The measured effect in test-suite work is instructive as an analogy: replacing physical cleanup with transaction-level rollback took a 447-test suite from roughly 246 seconds to under 3 seconds, because the rollback is a memory operation rather than disk I/O the transaction rollback case study. The same principle applies to approval: a cheap, atomic check at apply time beats an expensive reconstruction after the fact.

Rehearsing the proposal against a branch

Approval is a decision about the future, and the best way to make it concrete is to let the agent run the proposed operation somewhere harmless first. Database branching gives each task its own copy-on-write fork of the schema and data, so migrations and destructive steps run against a disposable copy and the parent stays untouched. The mechanism is copy-on-write: a branch reads the parent's blocks as they were and stores only the blocks it changes the branching explainer. PostgreSQL's own CREATE DATABASE ... TEMPLATE does the same thing at the engine level, copying from a template database that nobody connects to the branching explainer.

The operational discipline is what makes it useful: create the branch when the work starts, point the task's connection string at it, and delete both together, so an agent that wipes a table only wipes its own branch the database branching concept note. For a proposal queue, this gives you a rehearsal step: apply the payload to the branch, capture the resulting diff and row counts, and put the observed effect in the proposal next to the predicted effect. If they disagree, the proposal is wrong and the reviewer should see that before approving.

Keep branch rehearsal optional for R1 and R2 and mandatory for R3 and R4. The cost is a branch creation, which is cheap when storage is shared, and the benefit is that expected_effect stops being a guess. The failure mode this prevents is documented: an agent runs a partial migration, fails halfway, and leaves a schema that no version of the application expects, and a code revert does nothing to recover the data layer the TINE writeup.

Where the queue sits in your stack

The queue is infrastructure, not a feature of one agent. Two consequences follow.

First, every writing agent talks to the same submit endpoint, so there is one place to enforce the schema and one place to audit. Second, the proposal format becomes a contract you can test. Write a property test that no apply can succeed without a matching decision, and a test that a changed payload hash invalidates an old approval. These are the two bugs that actually bite.

If your agents run across several model providers, submitting and reading proposals means many small structured calls: classify the effect, draft the summary, check the diff. Routing those through a single API keeps the key management and spend in one place, which is what Quanta Inference is for. And if you would rather not build the routing policy and evidence pack from scratch, Quanta Experts can take the oversight workflow as a deliverable. Start small: one table, one route, one decision log, and a test that proves the apply step fails without an approval. That single test is worth more than any policy document.

FAQ

FAQ

What exactly is a write proposal?

It is a structured record of a planned change that an agent submits before modifying data. It carries the target rows, the expected effect, and the reversal cost, plus a payload hash and the requester context. The queue holds it until a routed approver decides, and the apply step consumes the decision.

Why not just log every agent write and review afterwards?

Because the point is to prevent the side effect, not to explain it later. Approval gates insert a verification checkpoint before side effects occur, which reduces the impact of hallucinated or mistaken tool calls the HITL workflow guide. After-the-fact review is a useful backstop, for example with database-level auditing the AWS pgAudit guide, but it cannot unsend an email.

How do I stop approvals from becoming a bottleneck?

Route by reversal class so most low-risk work auto-approves, and escalate only the hard and irreversible classes. Give reviewers one screen with the diff, the preconditions, and the rollback plan so a decision takes seconds, and support approve-with-edits so a reviewer does not restart the flow the HITL workflow guide.

What happens if the data changes between approval and apply?

The apply step must recheck the current state and refuse to write if the reviewed rows moved the preview playbook. Implement this with a version column or a hash of the reviewed fields, an expiry timestamp on the decision, and a distinct stale error that sends the proposal back for a fresh preview rather than silently retrying.

Can the agent approve its own proposal?

No, and the database should make it impossible. Give the agent role read-only privileges and give the apply service the only write grants, following the least-privilege pattern of per-use-case roles with narrowly granted object privileges the AWS roles guide. Then revoke update and delete on the decision table from the apply role, so neither the agent nor the apply path can rewrite the approval record.

Sources

Sources

  1. Tool Calling Has a Tax You're Not Seeing | by Duckweave
  2. Previewing an AI Agent's Changes Before It Runs Them
  3. Human-in-the-Loop AI Agents: How to Design Approval ...
  4. Using pgAudit to log database activity - AWS Documentation
  5. Managing PostgreSQL users and roles | AWS Database Blog
  6. How do you create a read-only user in PostgreSQL?
  7. PostgreSQL: Documentation: 9.1: GRANT
  8. How Database Transaction Rollback Revolutionized Test ...
  9. What is database branching? | AI coding agents
  10. cole-medin-knowledge-base/concepts/database-branching ...
  11. Database Branching for AI Agents: TINE Solves Schema ...
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles