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

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

Four guardrails between the checker's chosen outcome and the approval request A horizontal flow diagram. On the far left, an "Outcome chosen" box: the checker emitted an event with the claim, the category, and one of the sending outcomes — clear, confirm, review, or reject. Four guardrail gates sit in a row to the right, each drawn as a vertical bar. Gate 1: Resolve approver — looks up the per-category approval rule first (software goes to a manager, over-$250 goes to finance); if none, falls back to the claimant's team default approver; if still none, falls back to the configured admin. Returns a chat ID if one is set, otherwise an email address. Gate 2: Quiet hours — checks the local time against the policy-doc quiet-hours window (default 7pm to 8am); if outside business hours, defers the request to the next available business minute via an EventBridge one-off rule. Gate 3: Pick the card — clear and confirm get a one-tap card (Approve in a tap, open for detail); review gets the full card; reject gets a proposed-reject card with a draft note for the human to confirm. Gate 4: Final compose — fills the message from the voice doc for the outcome and category, attaches the reason line (in policy, or how far over and why), the amount, the receipt link, and Approve, Reject, and Ask buttons. After all four gates pass, the request ships via the chat webhook for the resolved chat ID, or via SES outbound for the resolved email. A note at the bottom: every request is logged to DynamoDB so the decision and its reason are on the record. Outcome clear, confirm, review, or reject Gate 1 Resolve approver category rule? team default? admin fallback? prefer chat ID, fall back to email Gate 2 Quiet hours local time in business window? if not, defer via Scheduler Gate 3 Pick the card clear/confirm → one-tap card review → full, reject → proposed with draft note Gate 4 Compose + reason voice template for category + amount, reason, receipt link, decision buttons Routing — chat message (preferred) or SES outbound email (fallback) chat webhook for chat · SES SendRawEmail for inbox every request logged to DDB ea-claims — the decision is on the record Every gate is a deterministic check — no model calls, no surprise behavior on a Tuesday afternoon.
Fig 4. Four guardrails between the outcome and the approval request. Resolve the approver. Honor quiet hours. Pick the right card. Compose with the reason. Then ship via chat or email and log the request so the decision is on the record.

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