How an expense claim finds its approver
The checker picked an outcome — clear, confirm, review, or reject. Now the routing piece has to figure out who should decide it, on what channel, at what time of day, and with what reason attached. Get any of those wrong and the request is worse than useless: a $400 software buy that lands on the wrong manager, an approval card with no reason, a ping at 11pm. Four small guardrails sit between the outcome and the request landing.
Key takeaways
- Approver resolution: per-category rule beats team default beats fallback to the configured admin.
- Chat messages are the default; email is the fallback if no chat ID is configured.
- Quiet hours defer a request to the next business hour so nothing lands at 2am.
- Every request ships with the claim, the amount, the category, the reason, the receipt, and buttons.
- Clear and confirm get a one-tap card; review gets the full card with the policy reason spelled out.
Four guardrails on every request
Gate 1: resolve the approver
Three places the routing Lambda looks for the approver, in order. First, the per-category approval rule in the policy doc — “software goes to the team manager,” “anything over $250 goes to finance.” The rule can depend on the outcome and the amount, so a clear meal and an over-limit software buy from the same person go to different approvers. Second, the claimant’s team default approver (“everyone on the design team reports to Dana”). Third, the configured admin fallback — the person who set up the approver and gets any claim that didn’t resolve. The fallback should never fire in steady state; if it does, the weekly digest names every claim that hit it so the policy doc can be fixed.
Once routing knows which person to ask, it looks up their delivery preference. The voice doc maps each approver to a chat ID if one is set, otherwise to an email address. Chat is preferred because a card with Approve, Reject, and Ask buttons is faster to act on than an email link. Email is the fallback so nobody’s claim falls through the cracks.
Gate 2: quiet hours
Claims arrive at all hours — the late client dinner gets submitted at 10pm, the airport taxi at midnight. The approval request for those shouldn’t buzz the approver’s phone the moment it lands. Gate 2 reads the policy doc’s quiet-hours setting (default 7pm to 8am, configurable per business). If the current local time is in the quiet window, the request creates a one-off EventBridge Scheduler rule that fires at the next business-hour minute and exits without sending. The Scheduler re-invokes the routing Lambda with the same payload at the deferred time, where Gate 2 lets it through.
The claim still records as submitted instantly — the claimant knows it’s in. Only the approver’s ping waits for business hours. An approval that can wait until 8am is worth waiting until 8am for.
Gate 3: pick the right card
Not every outcome deserves the same card. A clear and a confirm get a compact one-tap card: the claimant, the amount, the category, a thumbnail of the receipt, and an Approve button right there. The approver can tap Approve without opening anything, or tap through if they want the detail. A review gets the full card: the same fields plus the reason it needs a closer look, the policy limit it exceeded, and the claimant’s daily total in that category so far. A reject candidate gets a proposed-reject card — the reason and a draft note back to the claimant — that a human confirms or overrides.
The point of three card shapes is to spend the approver’s attention where it matters. The $36 lunch shouldn’t demand the same scrutiny as the $400 software buy, and making them look identical trains people to rubber-stamp both.
Gate 4: compose with the reason, then ship
The voice doc has one message template per category and outcome: a short message with placeholders for the claimant, amount, category, the reason line, and the receipt link. The routing Lambda fills the placeholders, attaches the decision buttons, and ships the message via the chat webhook. The webhook URL itself lives in Secrets Manager.
For email fallback, the same template is wrapped in a small HTML email with the same fields and a link that, when clicked, hits a Function URL that records the decision — the email equivalent of the chat buttons.
The reason line is the part that earns its place. “In policy — meals $36, under the $40 cap” tells the approver they can clear it with confidence. “$60 over the meals cap — review” tells them exactly what to weigh. The approver never has to reverse-engineer why this claim reached them; the reason travels with it.
Every request — chat or email, any outcome — updates the claim’s row in ea-claims in DynamoDB with the resolved approver, the channel, and the reason. The next post’s decision handler reads that row when the approver acts.
Why the guardrails exist
None of these gates are exotic. They’re the small care a thoughtful finance person would take by hand — send this to the right approver, don’t buzz them at midnight, match the scrutiny to the size of the claim, and always say why. Putting them in code as four small sequential gates makes them part of the design, not something you’re trusting whoever wrote any one message to remember.
Next post: how a claim actually gets paid once the approver acts — how Approve writes it to the payable sheet, how Reject sends a reason back, and how every action is logged so a reimbursement is auditable a year later.
All posts