What this workflow does
- Expense report submitted is a webhook protected by header authentication. It takes one report with its lines and receipt URLs.
- Read limits loads your limits table from Google Sheets: daily cap, receipt threshold, whether an itemized receipt is required and whether alcohol is reimbursable, per category.
- Read expense history loads past lines so the same receipt claimed by two people is caught.
- Split report lines turns the report into one item per line and rejects bodies without a report ID or lines.
- Download receipt fetches each receipt file. A failed or missing download is treated as a missing receipt rather than stopping the run.
- Build receipt request sends every PDF or image to Claude in one request, each labeled with its line ID, and restricts the returned line IDs to the ones in the report.
- Claude: read receipts returns merchant, date, total, tax, tip, currency, receipt type and an alcohol flag through structured outputs.
- Check hard rules compares each claimed line with its receipt and your limits, and assigns readable reason codes such as
claimed_$95.00_receipt_$85.00ormeals_over_daily_cap_$75.00. - Send to manager? routes normal reports to Message the manager as a direct message, and reports whose receipts could not be read, or that arrive without a manager Slack ID, to Post to finance channel.
- Append to expense log writes every line with its flags and a duplicate key for future checks.
Before you import
- A current n8n version.
- A source that can post the report JSON: your expense tool’s webhook or API through a small relay, a form, or a script. The expected body is documented at the top of Split report lines.
- A Google Sheet with “Limits” and “Expense log” tabs and the headers listed in the sticky note.
- An Anthropic API key and a Slack app with a bot token that can send direct messages.
Setup
- Import the JSON file and connect the Header Auth, Google Sheets, Anthropic and Slack credentials.
- Replace
REPLACE_WITH_SHEET_IDin Read limits, Read expense history and Append to expense log. - Replace
REPLACE_WITH_FINANCE_CHANNELin Post to finance channel. - If your receipt links need a login, add a credential to Download receipt (for example a bearer token for your expense tool’s API).
- Fill the Limits tab, for example
meals, 75, 25, yes, no. Lines in categories without a row use a $75 receipt threshold.
Test it
Post a three-line report: a hotel with a matching receipt, a meal whose receipt total differs from the claim, and a taxi dated after the trip with no receipt. Expect one Slack message listing the meal and taxi lines with reasons, and three rows in the log. Post the same hotel line again under a new report ID and confirm it is flagged as a duplicate of the first report.
Make it production-ready
- Error workflow. Add one in the workflow settings so failed runs reach finance.
- Soft policy check. Add a second Claude call that compares unusual lines against your numbered written policy and must cite a clause. Keep the hard rules in code.
- Manager actions. Replace the direct message with a Slack approval step, and write the decision back to the log’s
manager_decisioncolumn so you can see which flags are noise. - Image hashing. The duplicate key uses merchant, date and amount. Add a perceptual hash of receipt images if people resubmit edited copies.
- Limits. Large reports with many receipts make one big request. Split reports above 20 receipts into batches.
- Model choice.
claude-haiku-5-5is a cheaper option for high volumes of clear receipts. Re-test on crumpled and foreign receipts before switching.
Read the full guide, how to automate expense report review with AI, or talk to Lumeor if you want help connecting Expensify, Ramp or Brex.