Schedule icon
OutputValues icon
Downloads icon
Script icon
Loop icon
If icon
ChatCompletion icon
Anthropic icon
SlackIncomingWebhook icon
Pause icon
Upload icon
Set icon

Expense Report Auditor with Policy Checks, Duplicate Receipt Detection and AI Receipt Reading

Audit every expense claim against policy, catch duplicate and mismatched receipts with AI, and route only the exceptions to finance for approval.

Categories
AIBusinessData

Finance teams usually spot-check only 10 to 20 percent of expense reports, because reviewing one takes around 20 minutes. Duplicate receipts, inflated amounts and policy breaks slip through, and AI-generated fake receipts now make up most flagged expense fraud. This blueprint audits every claim of the month. It approves the clean ones on its own and sends only the exceptions to a person, with the reason for each. It's for finance and operations teams that reimburse employees from an expense tool or a spreadsheet export.

How it works

  1. period picks the month to audit, from the month input or the month before the run. fetch (io.kestra.plugin.aws.s3.Downloads) downloads <prefix>/<YYYY-MM>/claims.csv and every receipt from any S3-compatible bucket.

  2. rules (io.kestra.plugin.scripts.python.Script) checks every claim against policy:

    • daily limits per category;
    • a missing receipt above the threshold, or a receipt file that isn't in the bucket;
    • weekend spending and late submissions;
    • duplicates within the month.

    It also fingerprints every receipt, both the exact file and its merchant, amount and date, and compares them with up to 13 months of earlier receipts kept in the KV store, so a receipt submitted again months later is caught.

  3. read_receipts (io.kestra.plugin.core.flow.Loop, four at a time) sends each receipt, image or PDF, to io.kestra.plugin.ai.completion.ChatCompletion with the Anthropic provider as an image or PDF content block. The model reports what the receipt actually shows: the total, the date, the merchant, any alcohol, and signs of tampering or generation. A receipt it can't process is flagged instead of stopping the audit.

  4. assess compares the receipt with the claim in code, with tolerances: amount, date and merchant. It merges this with the policy checks and writes audit.csv and summary.md. A claim is auto-approved only when nothing at all is flagged.

  5. post_summary posts the result to Slack. Outside a dry run, when something is flagged, ask_reviewer posts a link and approval (io.kestra.plugin.core.flow.Pause) shows finance a form whose options are this month's flagged claims, with their reasons. Reviewers pick the claims to reject and those to send back to the employee, and every other flagged claim is approved. Without a decision within seven days, the execution is cancelled.

  6. decisions writes one decision per claim with who decided, upload_decisions (io.kestra.plugin.aws.s3.Upload) stores it as <prefix>/<YYYY-MM>/audit/decisions.csv, and remember_receipts (io.kestra.plugin.core.kv.Set) saves the month's receipt fingerprints. alert_failure posts any failure to Slack.

No money moves: the output is a decisions file for your payroll or expense tool to pick up. Claim text and receipt contents travel as base64 between tasks, so nothing an employee types is ever evaluated as a Kestra expression.

Prerequisites

  • An S3-compatible bucket (AWS S3, MinIO, SeaweedFS, Cloudflare R2 and others) with one folder per month: <prefix>/<YYYY-MM>/claims.csv with the columns claim_id, employee, date, merchant, category, amount, currency, receipt_file, description, and the receipts in <prefix>/<YYYY-MM>/receipts/ as PNG, JPG or PDF. Most expense tools can export this.
  • Docker on the worker for the Python script tasks.
  • Secrets:
    • S3_ACCESS_KEY_ID and S3_SECRET_KEY_ID: credentials that can read the month's folder and write audit/decisions.csv.
    • ANTHROPIC_API_KEY: Anthropic API key for reading receipts.
    • SLACK_WEBHOOK_URL: Slack incoming webhook for the summary, the review request, the decisions and failure alerts.

Inputs

  • bucket (STRING, default expenses) and prefix (STRING, default claims): where the monthly folders live.
  • month (STRING, optional): YYYY-MM. Empty means the month before the run.
  • s3_endpoint (STRING, default https://s3.us-east-1.amazonaws.com) and region (STRING, default us-east-1): use your own endpoint for MinIO, SeaweedFS or R2.
  • policy (JSON): daily_limits per category, receipt_required_above, flag_weekends, max_age_days and alcohol_reimbursable.
  • max_receipts (INT, default 200): the most receipts read by the model in one run. Claims beyond it are flagged for manual review.
  • dry_run (BOOL, default true): audit and report only. No review request, no decisions file, and no fingerprints saved.

Quick start

  1. Add the four secrets and save the flow.
  2. Run it with month set to a past month and dry_run left on. Read summary.md and audit.csv in the assess outputs, and adjust policy.
  3. Run the previous month with dry_run set to false to seed the receipt fingerprints, then the current month, and decide the flagged claims in the form.
  4. Enable the monthly trigger to audit each month on the 2nd.

Outputs

  • {{ outputs.audit_file }} / {{ outputs.assess.outputFiles['audit.csv'] }}: every claim with what the receipt shows, its flags and status.
  • {{ outputs.assess.outputFiles['summary.md'] }} and {{ outputs.assess.vars.summary }}: the human summary.
  • {{ outputs.flagged_claims }} / {{ outputs.assess.vars.flagged }}, plus held_amount and total_amount.
  • {{ outputs.decisions_file }} / {{ outputs.decisions.outputFiles['decisions.csv'] }} and {{ outputs.decisions.vars.decisions }}: the decision for every claim, also stored in the bucket.
  • KV key expense-receipt-fingerprints-<bucket>: 13 months of receipt fingerprints.

Common pitfalls

  • Duplicate detection across months only starts after the first non-dry run has saved fingerprints. Run the previous month once first.
  • Policy limits apply per claim. Split meals such as a team dinner need a higher meals limit, or are reviewed as flagged.
  • The model can misread poor photos. Any mismatch is flagged for a person, never rejected automatically.
  • Set the claude_model variable to change the model. Reading one receipt is one short multimodal call.

How to extend

  • Load decisions.csv into your payroll or ERP with a database or HTTP task after upload_decisions.
  • Email employees whose claims need information, using the decisions output.
  • Add per-employee or per-department limits to policy and the rules script.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.