AI Automation & Agents  

AI Agents in Practice: Expense Report Auditor & Reimbursement Agent (Prompts + Code)

Introduction

Here’s a second, production-grade agent you can lift into your stack today: an Expense Report Auditor & Reimbursement Agent. It reads a submitted expense, checks policy eligibility, flags issues, and—when allowed—creates a reimbursement. As before, the agent never claims success unless the backend executed the action and returned a receipt.


The Use Case

Employees submit expenses (amount, category, merchant, date, receipt image URL). The agent should verify policy limits, check duplicate submissions, validate receipt presence, and either propose a reimbursement or request corrections. All decisions must cite policy facts; all actions must yield receipts.


Prompt Contract (the agent’s interface)

# file: contracts/expense_auditor_v1.yaml
role: "ExpenseAuditor"
scope: >
  Audit a single expense. Use eligible claims for policy facts and prior submissions.
  Ask once for missing fields (amount_cents, category, date, merchant, receipt_url).
  Propose tool calls; never assert success without a receipt.
output:
  type: object
  required: [summary, findings, decision, citations, tool_proposals]
  properties:
    summary: {type: string, maxWords: 60}
    findings: {type: array, items: {type: string, maxWords: 18}, maxItems: 8}
    decision: {type: string, enum: ["approve","approve_with_adjustment","reject","need_more_info"]}
    citations: {type: array, items: {type: string}}
    tool_proposals:
      type: array
      items:
        type: object
        required: [name, args, preconditions, idempotency_key]
        properties:
          name: {type: string, enum: [CheckDuplicates, CheckPolicy, CreateReimbursement]}
          args: {type: object}
          preconditions: {type: string}
          idempotency_key: {type: string}
policy_id: "expense_policy.v5.us"
citation_rule: "1-2 minimal-span claim_ids per factual sentence"
decoding:
  narrative: {top_p: 0.92, temperature: 0.72, stop: ["\n\n## "]}
  bullets:   {top_p: 0.82, temperature: 0.45}

Example claims (context shown to the model)

[
  {"claim_id":"policy:meal:cap_usd","text":"Meal expenses capped at $75 per person per day.",
   "effective_date":"2025-06-01","source_id":"doc:policy_v5","span":"capped at $75"},
  {"claim_id":"policy:receipt_required_over_25","text":"Receipts required for any expense over $25.",
   "effective_date":"2025-06-01","source_id":"doc:policy_v5","span":"over $25"},
  {"claim_id":"policy:no_alcohol_reimbursed","text":"Alcohol is not reimbursable.",
   "effective_date":"2025-06-01","source_id":"doc:policy_v5","span":"not reimbursable"},
  {"claim_id":"history:merchant:dup_hash:2025-10-04","text":"Expense hash MERCH123-2025-10-04-2599 filed by user U007.",
   "effective_date":"2025-10-04","source_id":"sys:expenses","span":"filed by user U007"}
]

Tool Interfaces (typed, with receipts)

# tools.py
from pydantic import BaseModel, Field, HttpUrl
from datetime import date
from typing import Optional, Dict

class CheckDuplicatesArgs(BaseModel):
    user_id: str
    merchant: str
    amount_cents: int
    expense_date: date

class CheckPolicyArgs(BaseModel):
    category: str
    amount_cents: int
    attendees: Optional[int] = Field(default=1)
    has_receipt: bool
    contains_alcohol: bool

class CreateReimbursementArgs(BaseModel):
    user_id: str
    amount_cents: int
    expense_id: str
    memo: str

class ToolReceipt(BaseModel):
    tool: str
    ok: bool
    ref: str
    message: str = ""
    data: Optional[Dict] = None
# adapters.py (demo logic)
from tools import *
from datetime import date

DUP_DB = {("U007","MERCH123",2599,date(2025,10,4)): "exp-abc"}  # prior expense key
MEAL_CAP_CENTS = 7500  # $75

def check_duplicates(a: CheckDuplicatesArgs) -> ToolReceipt:
    key = (a.user_id, a.merchant, a.amount_cents, a.expense_date)
    if key in DUP_DB:
        return ToolReceipt(tool="CheckDuplicates", ok=False, ref="dup-found",
                           message="Duplicate submission", data={"prior_expense_id": DUP_DB[key]})
    return ToolReceipt(tool="CheckDuplicates", ok=True, ref="dup-none", message="No duplicate detected")

def check_policy(a: CheckPolicyArgs) -> ToolReceipt:
    if a.amount_cents > 2500 and not a.has_receipt:
        return ToolReceipt(tool="CheckPolicy", ok=False, ref="pol-receipt", message="Receipt required > $25")
    if a.category.lower() == "meal" and a.amount_cents > MEAL_CAP_CENTS * a.attendees:
        return ToolReceipt(tool="CheckPolicy", ok=False, ref="pol-meal-cap",
                           message="Exceeds meal cap", data={"cap_cents": MEAL_CAP_CENTS * a.attendees})
    if a.contains_alcohol:
        return ToolReceipt(tool="CheckPolicy", ok=False, ref="pol-alcohol", message="Alcohol not reimbursable")
    return ToolReceipt(tool="CheckPolicy", ok=True, ref="pol-ok", message="Eligible under policy")

def create_reimbursement(a: CreateReimbursementArgs) -> ToolReceipt:
    return ToolReceipt(tool="CreateReimbursement", ok=True, ref=f"rb-{a.expense_id}",
                       message="Reimbursement created", data={"amount_cents": a.amount_cents})

Agent Loop (proposal → verification → execution → receipts)

# agent_expense.py
import uuid, json
from datetime import date
from typing import Any, Dict, List
from pydantic import BaseModel, ValidationError
from tools import *
from adapters import *

ALLOWED_TOOLS = {"CheckDuplicates","CheckPolicy","CreateReimbursement"}

def new_idem() -> str:
    return f"idem-{uuid.uuid4()}"

def verify_proposal(p: Dict[str, Any]) -> str:
    required = {"name","args","preconditions","idempotency_key"}
    if not required.issubset(p): return "Missing proposal fields"
    if p["name"] not in ALLOWED_TOOLS: return "Tool not allowed"
    if p["name"] == "CreateReimbursement" and p["args"].get("amount_cents", 0) <= 0:
        return "Amount must be positive"
    return ""

def execute(p: Dict[str, Any]) -> ToolReceipt:
    n, a = p["name"], p["args"]
    if n == "CheckDuplicates":
        a["expense_date"] = date.fromisoformat(a["expense_date"])
        return check_duplicates(CheckDuplicatesArgs(**a))
    if n == "CheckPolicy":
        return check_policy(CheckPolicyArgs(**a))
    if n == "CreateReimbursement":
        return create_reimbursement(CreateReimbursementArgs(**a))
    return ToolReceipt(tool=n, ok=False, ref="none", message="Unknown tool")

# ---- Pretend model call that returns a plan matching the contract ----
def call_model(contract_yaml: str, claims: List[Dict[str,Any]], expense: Dict[str,Any]) -> Dict[str,Any]:
    needs_adjust = expense["category"]=="meal" and expense["amount_cents"]>7500*expense.get("attendees",1)
    approved_amount = min(expense["amount_cents"], 7500*expense.get("attendees",1))
    decision = "approve_with_adjustment" if needs_adjust else "approve"
    return {
      "summary": "Expense audited; eligible with cap applied.",
      "findings": ["Receipt present","No duplicate found","Meal cap applied if needed"],
      "decision": decision,
      "citations": ["policy:meal:cap_usd","policy:receipt_required_over_25","policy:no_alcohol_reimbursed"],
      "tool_proposals": [
        {"name":"CheckDuplicates",
         "args":{"user_id":expense["user_id"],"merchant":expense["merchant"],
                 "amount_cents":expense["amount_cents"],"expense_date":expense["date"]},
         "preconditions":"Same user+merchant+date+amount not submitted.",
         "idempotency_key": new_idem()},
        {"name":"CheckPolicy",
         "args":{"category":expense["category"],"amount_cents":expense["amount_cents"],
                 "attendees":expense.get("attendees",1),
                 "has_receipt": bool(expense.get("receipt_url")),
                 "contains_alcohol": expense.get("contains_alcohol", False)},
         "preconditions":"Receipt present if >$25; meal under cap; no alcohol.",
         "idempotency_key": new_idem()},
        {"name":"CreateReimbursement",
         "args":{"user_id":expense["user_id"],
                 "amount_cents": approved_amount if decision.startswith("approve") else 0,
                 "expense_id": expense["expense_id"],
                 "memo": f"{expense['category']} at {expense['merchant']}"},
         "preconditions":"Policy ok and no duplicate.",
         "idempotency_key": new_idem()}
      ]
    }

def render_response(model_json: Dict[str,Any], receipts: List[ToolReceipt]) -> str:
    idx = {r.tool:r for r in receipts}
    out = []
    out.append(model_json["summary"])
    out.append("")
    out.append("Findings:")
    for f in model_json["findings"]:
        out.append(f"• {f}")
    out.append(f"\nDecision: {model_json['decision']}")
    if "CreateReimbursement" in idx and idx["CreateReimbursement"].ok:
        amt = idx["CreateReimbursement"].data["amount_cents"] / 100
        out.append(f"Reimbursement created: ${amt:.2f} (ref {idx['CreateReimbursement'].ref})")
    elif "CreateReimbursement" in idx and not idx["CreateReimbursement"].ok:
        out.append(f"Reimbursement not created: {idx['CreateReimbursement'].message}")
    out.append("\nCitations: " + ", ".join(model_json["citations"]))
    return "\n".join(out)

def handle(expense: Dict[str,Any]) -> str:
    contract = open("contracts/expense_auditor_v1.yaml").read()
    claims: List[Dict[str,Any]] = []  # plug real claim loader (policy + user history)
    plan = call_model(contract, claims, expense)

    receipts: List[ToolReceipt] = []
    for prop in plan["tool_proposals"]:
        reason = verify_proposal(prop)
        if reason:
            receipts.append(ToolReceipt(tool=prop["name"], ok=False, ref="blocked", message=reason)); continue
        rec = execute(prop)
        receipts.append(rec)
        if not rec.ok and prop["name"] in {"CreateReimbursement"}:
            break
    return render_response(plan, receipts)

if __name__ == "__main__":
    example_expense = {
      "expense_id":"exp-999",
      "user_id":"U007",
      "merchant":"MERCH123",
      "category":"meal",
      "amount_cents": 9800,
      "date":"2025-10-18",
      "attendees":1,
      "receipt_url":"https://receipts/r1.png",
      "contains_alcohol": False
    }
    print(handle(example_expense))

The Prompt You’d Send to the Model (concise and testable)

System:
You are ExpenseAuditor. Follow the contract:
- Audit the expense; ask once if required fields are missing.
- Cite 1–2 claim_ids per factual sentence using provided claims.
- Propose tools; never assert success without a receipt.
- Output JSON with keys: summary, findings[], decision, citations[], tool_proposals[].

Claims (eligible only):
[ ... JSON array of claim records like above ... ]

User:
Please audit this expense and reimburse if eligible:
{"expense_id":"exp-999","user_id":"U007","merchant":"MERCH123","category":"meal",
 "amount_cents":9800,"date":"2025-10-18","attendees":1,
 "receipt_url":"https://receipts/r1.png","contains_alcohol":false}

How to adapt fast

Swap the mock adapters with your finance APIs, enforce idempotency on payouts, and load claims from your policy docs and expense history with freshness windows. Add a validator step before execution to enforce schema, lexicon, locale, citation coverage, and “no implied writes.” Ship the contract, policy bundle, and decoder settings behind a flag with canary + rollback.