Don't Let the Agent Post the Journal Entry: Designing Approval Boundaries for Finance AI
AI agents are getting good at the parts of finance work that used to require a person to copy values between systems: finding invoices, matching transactions, explaining variances, and drafting reconciliations.
That progress creates an awkward engineering question: where should an agent be allowed to act, and where should it only prepare a decision for a human?
In accounting workflows, the most useful answer is rarely “autonomous” or “manual.” It is a set of explicit approval boundaries backed by evidence, deterministic rules, and an audit trail.
This article walks through a practical design for an agent-assisted close workflow. The examples use invoice and bank-transaction matching, but the same pattern applies to expense review, revenue operations, and fintech support workflows.
Start with a proposal, not a side effect
A dangerous agent tool looks like this:
await ledger.createJournalEntry({
account: "6100",
amount: 12840.50,
memo: "March software expenses"
});
The call is easy to write, but it collapses several different decisions into one irreversible operation:
- Which source records support the amount?
- Is the period open?
- Does the proposed account require approval?
- Has another worker already posted an entry?
- Who is accountable if the source data is incomplete?
Instead, make the agent produce a typed proposal first:
type JournalProposal = {
id: string;
period: string;
lines: Array<{
account: string;
debit: number;
credit: number;
}>;
sourceIds: string[];
confidence: number;
exceptions: string[];
approvalState: "pending" | "approved" | "rejected";
};
The proposal is a durable object. It can be inspected, compared with previous proposals, approved, rejected, or regenerated. Posting is a separate command that accepts only an approved proposal ID.
That separation is more important than the model you use. A better prompt cannot compensate for an API that lets an uncertain suggestion mutate the ledger directly.
Build the evidence bundle before asking for a decision
An agent should not return a confidence score without showing what produced it. For a transaction match, an evidence bundle might contain:
{
"transaction_id": "bank_8421",
"candidate_invoice_id": "inv_1092",
"signals": [
{"name": "amount_match", "value": true},
{"name": "vendor_match", "value": true},
{"name": "date_delta_days", "value": 2},
{"name": "currency_match", "value": true}
],
"missing": [],
"source_links": [
"/bank/transactions/bank_8421",
"/ap/invoices/inv_1092"
]
}
The model can summarize this bundle in plain language, but the application should calculate critical facts itself. Amount equality, period status, duplicate detection, and debit-credit balance are poor candidates for free-form model judgment.
A useful rule is: let the model interpret evidence, but let deterministic code establish the invariants.
This also makes review faster. A reviewer should be able to answer “why is this proposed?” without opening six unrelated tabs or trusting a paragraph generated from hidden context.
Use risk tiers instead of one global confidence threshold
A 98% match confidence does not mean the same thing for every action. Posting a small recurring software invoice is different from changing a revenue account or closing a period.
Define risk using business attributes, not only model output:
function requiredApproval(proposal: JournalProposal, context: Context) {
if (context.periodClosed) return "blocked";
if (proposal.exceptions.length > 0) return "senior_review";
if (context.isRevenueAccount) return "accounting_review";
if (context.amount > 10000) return "accounting_review";
if (proposal.confidence >= 0.98) return "auto_approve_eligible";
return "standard_review";
}
“Auto-approve eligible” still does not have to mean “post immediately.” It can mean the proposal enters a queue where a policy engine, rather than a language model, performs the final check. If your organization is not ready for that, every tier can remain human-approved while the workflow still saves preparation time.
Make tools narrow, typed, and observable
Agent tool design is security design. Prefer several small tools over one privileged “do finance” function:
-
search_source_records(filters)returns read-only records. -
create_reconciliation_proposal(input)stores a proposal. -
request_approval(proposalId)changes workflow state. -
post_approved_proposal(proposalId, idempotencyKey)performs the side effect. -
record_review(proposalId, decision, reason)stores human reasoning.
Every tool call should log the actor, timestamp, input references, output references, and authorization result. Do not log raw credentials or entire documents by default. Store stable IDs and links to the source system instead.
The posting tool also needs idempotency. A worker may time out after the ledger accepts a request, then retry. The idempotency key should be derived from the proposal and posting operation, not generated afresh on every attempt:
const key = `post:${proposal.id}:v${proposal.version}`;
await ledger.postApprovedProposal(proposal.id, { idempotencyKey: key });
Without this, “reliable retry” can become duplicate accounting.
Treat the exception queue as a product surface
The workflow is not successful because an agent produced a high number of matches. It is successful when people can resolve the uncertain remainder quickly and understand why those items were routed to them.
An exception should include:
- The proposed action.
- The evidence used.
- The exact rule or missing data that blocked automation.
- Suggested next actions.
- A place to record the final decision.
That last piece matters. “Rejected” is less useful than “rejected because the invoice belongs to the prior period.” Review decisions become feedback for rules, reporting, and future agent evaluations.
At Portali, this is the practical distinction between adding an AI chat box to accounting software and building an evidence-linked workflow. The interface can stay simple, but every suggestion should lead back to its source records and its review state. More context is available at portali.tech, but the architectural principle stands independently of any product: keep the decision visible.
Measure the boundary, not just the model
Track metrics that tell you whether the approval design is working:
- Proposal acceptance rate by workflow and risk tier.
- Percentage of proposals with complete source evidence.
- Average time from exception creation to resolution.
- Duplicate or reversed postings.
- Human edits between proposal and approval.
- Reasons for rejection and escalation.
A model benchmark may improve while the accounting workflow gets worse. For example, a more confident model could increase acceptance while hiding a rise in period errors. Operational metrics expose that tradeoff.
A safer definition of autonomy
For finance agents, autonomy should mean fewer repetitive decisions for people, not fewer people in the control loop. The durable pattern is straightforward:
- Read source systems through narrow, read-only tools.
- Produce an evidence-linked proposal.
- Validate invariants with deterministic code.
- Route the proposal according to explicit risk policy.
- Require approval before consequential side effects.
- Post with idempotency and a complete audit trail.
- Learn from review outcomes without silently changing policy.
That design lets an agent move quickly without making the ledger mysterious. The best finance automation is not the system that acts most often. It is the system that makes safe action obvious, risky action deliberate, and every decision explainable after the close is over.
Top comments (0)