Part 5 of 7 · Expense approver series ~5 min read

How an expense claim gets paid

An approval card lands in Dana’s chat at 8:03am. Sam’s client lunch, $36, in policy, receipt attached. There are three buttons: Approve, Reject, Ask. What happens when she taps one? The honest answer is “it depends on which one.” This post walks through the three things an approver can do — approve, reject, ask — and how the claim record, the payable sheet, and the audit trail all stay in sync. And the one thing that never happens: the system moving money on its own.

Key takeaways

  • Three actions per card: approve (write to the payable sheet), reject (reason back to the claimant), ask (request more).
  • Approve writes a row to a payable sheet that your bookkeeper or payroll run reads — the system never pays anyone.
  • Reject sends a plain-English reason to the claimant so they know why and what to fix.
  • Ask requests a missing receipt or a note, and parks the claim until the claimant replies.
  • Every action writes a before-and-after snapshot to the audit table, so each decision is reversible.

Three actions on the card

Three actions on the approval card A diagram showing one input on the left flowing through an approval card, then branching into three action paths. Far left: an "Approval card in chat" box showing a typical card — claimant, amount, category, the policy reason, a receipt link — with three button placeholders below: Approve, Reject, Ask. The approver taps one button. The middle column shows the three branches. Branch one, Approve: a Function URL Lambda marks the claim approved, writes a row to the payable sheet in Drive via the Sheets API (employee, amount, category, approver, receipt link), notifies the claimant that it is approved, and writes an approved event to the audit trail. The system does not move money — the payable sheet is what a human payroll run or bookkeeper reads from. Branch two, Reject: opens a small note field pre-filled with the policy reason; on send, a Function URL Lambda marks the claim rejected, sends the reason to the claimant so they know why and what to fix, and writes a rejected event. Branch three, Ask: requests a missing receipt or a note; the claim is parked in a waiting state and a message goes to the claimant; when the claimant replies through the same submit lane, the claim re-enters the checker. The right side shows the convergence: every action writes a row to the ea-audit DynamoDB table with timestamp, claim id, action, by-user, and a before-and-after snapshot. A note at the bottom: Approve writes to the payable sheet — it never sends a payment. A person still runs the actual reimbursement. Card in chat amount, reason, link [Approve] [Reject] [Ask] Action 1 Approve • Mark claim approved • Write to payable sheet via Sheets API • Notify the claimant Action 2 Reject • Note field, pre-filled with the policy reason • Reason sent to the claimant, what to fix Action 3 Ask • Request a receipt or a note • Claim parked until the claimant replies Audit trail DynamoDB ea-audit timestamp · claim_id action · by-user before / after Approve writes to the payable sheet — it never sends a payment. A person runs the reimbursement.
Fig 5. Three actions per card, three different effects. Approve writes the claim to the payable sheet. Reject sends a reason to the claimant. Ask requests more and parks the claim. Every action writes to the audit trail, and none of them moves money.

Action 1: approve (the most common)

Dana reads the card — Sam’s $36 lunch, in policy, receipt attached — and taps Approve. The tap submits to a Function URL Lambda. Three things happen, in order. First, the claim’s row in ea-claims is marked approved with Dana’s identity and the timestamp. Second, a row is appended to the payable sheet in Drive via the Sheets API: employee, amount, category, approver, date, and a link to the receipt. Third, an action: approved row is written to ea-audit with the user, timestamp, and the claim snapshot. Sam gets a one-line note: “Your $36 lunch claim is approved.”

The payable sheet is the boundary the system never crosses. It does not call a bank. It does not trigger a transfer. It writes a clean, approved, audit-trailed row, and whoever runs payroll or the weekly reimbursement batch reads that sheet and pays from it. Keeping the system one step back from the money is the whole point — the riskiest action in the pipeline stays in human hands, and the system’s job is to make that human’s decision fast and well-grounded.

Action 2: reject (with a reason)

Sometimes the claim shouldn’t be paid. The $90 dinner that’s well over the meals cap with no business reason given. The personal item that slipped into the wrong category. The duplicate of a claim already submitted last week. Dana taps Reject. A small note field opens, pre-filled with the policy reason the checker already worked out (“$50 over the $40 meals cap”), which Dana can edit or add to. On send, a Function URL Lambda marks the claim rejected, sends the reason to Sam so he knows exactly why and what to do (“split the personal portion and resubmit,” say), and writes a rejected audit row.

The reason matters because a rejection without one just breeds a follow-up question and a grumble. “Rejected” on its own feels arbitrary; “rejected because it’s $50 over the meals cap, please resubmit the business portion” is a clear next step. The claimant is never left guessing.

Action 3: ask (the “I need more”)

Sometimes the approver can’t decide yet. The receipt photo is too blurry to read. The amount is fine but there’s no note about which client the dinner was for. The category looks off. Dana taps Ask, picks what she needs (“clearer receipt,” “which client?”), and the claim is parked in a waiting state. A message goes to Sam asking for exactly that.

When Sam replies — through the same submit lane he used in Part 2, by re-uploading the receipt or adding the note — the claim re-enters the checker, gets re-evaluated against policy, and comes back to the approver with the new information attached. The claim doesn’t leave the system or get lost; it just pauses until the missing piece arrives. Ask is the pressure valve that keeps a half-complete claim from being either wrongly approved or unfairly rejected.

Every action is logged, every action is reversible

The ea-audit table records every approve, reject, and ask with the user who took the action, the timestamp, and a snapshot of the claim before and after. If a claim was approved in error — wrong amount, wrong person, approved twice — an admin can run an “undo last action” that reads the previous-state snapshot, restores the claim, and removes the row from the payable sheet if it hasn’t been paid yet. The undo is itself an audit row, so the trail of edits stays clean.

This kind of reversibility matters because expenses get audited. When a year-end review asks “who approved this $400 software buy and why?”, the audit trail answers in one query: the approver, the time, the reason, and the policy that applied. The trail is the memory the business has when the people who made the call have moved on.

Next post: the cost breakdown. The whole pipeline above runs in coffee-money territory at SMB volume; Part 6 explains exactly where the dollars go and why the policy check itself is almost free.

All posts