Introduction
Here’s a fifth, production-grade agent pattern for you to implement: an Invoice Approval & Payment Agent. This agent reads submitted invoices, checks compliance with payment policies, validates approvals, and either authorizes payment or requests additional approvals. It operates on the principle of least privilege—never authorizing payments without approval from the appropriate individuals or teams, and never claiming success without executing the payment action and returning a receipt.
The Use Case
An invoice is submitted by a vendor (containing invoice number, amount, due date, and other details). The agent must ensure the invoice follows payment policies (e.g., within budget, approved by the right person), confirm that the correct payment method is used, and—if everything checks out—approve the payment or request further actions like approvals or payment corrections. All actions should be tracked with receipts, and the decision-making process should be auditable.
Prompt Contract (the agent’s interface)
# file: contracts/invoice_approval_v1.yaml
role: "InvoiceApprovalAgent"
scope: >
Verify and approve invoices based on payment policies. Ask once if critical fields are missing (invoice_id, amount, vendor, due_date).
Propose tool calls for compliance checks, approvals, and payments; never assert success without a receipt.
output:
type: object
required: [summary, decision, invoice_details, citations, next_steps, tool_proposals]
properties:
summary: {type: string, maxWords: 80}
decision: {type: string, enum: ["approve","reject","need_approval","need_more_info"]}
invoice_details:
type: object
required: [invoice_id, vendor, amount_cents, due_date, payment_method]
properties:
invoice_id: {type: string}
vendor: {type: string}
amount_cents: {type: integer}
due_date: {type: string}
payment_method: {type: string, enum: ["bank_transfer", "check", "credit_card"]}
citations: {type: array, items: {type: string}}
next_steps: {type: array, items: {type: string}, maxItems: 6}
tool_proposals:
type: array
items:
type: object
required: [name, args, preconditions, idempotency_key]
properties:
name: {type: string, enum: [CheckInvoicePolicy, CheckApproval, AuthorizePayment, RequestApproval]}
args: {type: object}
preconditions: {type: string}
idempotency_key: {type: string}
policy_id: "payment_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 given to the model)
[
{"claim_id":"policy:invoice:payment_cap","text":"Invoices exceeding $10,000 must be approved by the CFO.",
"effective_date":"2025-06-01","source_id":"doc:payment_policy_v5","span":"$10,000 must be approved by the CFO"},
{"claim_id":"policy:invoice:payment_method","text":"Only bank transfers or checks are allowed for amounts over $5,000.",
"effective_date":"2025-06-01","source_id":"doc:payment_policy_v5","span":"bank transfers or checks"},
{"claim_id":"policy:vendor:approval_required","text":"Vendor X requires manager approval for invoices exceeding $1,000.",
"effective_date":"2025-06-01","source_id":"doc:payment_policy_v5","span":"Vendor X requires manager approval"},
{"claim_id":"vendor:acme_inc","text":"Vendor ACME Inc has payment terms of Net 30.",
"effective_date":"2025-06-01","source_id":"vendor_data","span":"Net 30"}
]
Tool Interfaces (typed, with receipts)
# tools.py
from pydantic import BaseModel
from typing import Optional, Dict
from datetime import date
class CheckInvoicePolicyArgs(BaseModel):
invoice_id: str
amount_cents: int
vendor: str
payment_method: str
due_date: date
class CheckApprovalArgs(BaseModel):
invoice_id: str
amount_cents: int
vendor: str
approver_id: str
class AuthorizePaymentArgs(BaseModel):
invoice_id: str
amount_cents: int
payment_method: str
due_date: date
class RequestApprovalArgs(BaseModel):
invoice_id: str
amount_cents: int
vendor: str
approver_id: 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, timedelta
VENDORS = {"Acme Inc": {"max_approval": 1000, "payment_method": "bank_transfer", "manager_id": "M001"}}
APPROVALS = {("Acme Inc", 1500): "appr-approve"}
def check_invoice_policy(a: CheckInvoicePolicyArgs) -> ToolReceipt:
if a.amount_cents > 10000 and a.payment_method != "bank_transfer" and a.payment_method != "check":
return ToolReceipt(tool="CheckInvoicePolicy", ok=False, ref="policy-invalid-method",
message="Invalid payment method for invoice over $10,000")
if a.amount_cents > VENDORS[a.vendor]["max_approval"]:
return ToolReceipt(tool="CheckInvoicePolicy", ok=False, ref="policy-approval-required",
message="Requires approval for invoices over $1,000")
return ToolReceipt(tool="CheckInvoicePolicy", ok=True, ref="policy-ok", message="Invoice meets policy")
def check_approval(a: CheckApprovalArgs) -> ToolReceipt:
approval = APPROVALS.get((a.vendor, a.amount_cents))
if approval:
return ToolReceipt(tool="CheckApproval", ok=True, ref=approval, message="Approval found")
return ToolReceipt(tool="CheckApproval", ok=False, ref="no-approval", message="Approval required")
def authorize_payment(a: AuthorizePaymentArgs) -> ToolReceipt:
return ToolReceipt(tool="AuthorizePayment", ok=True, ref=f"payment-{a.invoice_id}",
message="Payment authorized", data={"amount_cents": a.amount_cents})
def request_approval(a: RequestApprovalArgs) -> ToolReceipt:
return ToolReceipt(tool="RequestApproval", ok=True, ref=f"approval-{a.invoice_id}",
message="Approval requested", data={"approver_id": a.approver_id})
Agent Loop (proposal → verification → execution → receipts)
# agent_invoice.py
import uuid, json
from datetime import date
from typing import Any, Dict, List
from tools import *
from adapters import *
ALLOWED_TOOLS = {"CheckInvoicePolicy","CheckApproval","AuthorizePayment","RequestApproval"}
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"] == "AuthorizePayment" and "payment_method" not in p["args"]:
return "Payment method required"
return ""
def execute(p: Dict[str, Any]) -> ToolReceipt:
n, a = p["name"], p["args"]
if n == "CheckInvoicePolicy": return check_invoice_policy(CheckInvoicePolicyArgs(**a))
if n == "CheckApproval": return check_approval(CheckApprovalArgs(**a))
if n == "AuthorizePayment": return authorize_payment(AuthorizePaymentArgs(**a))
if n == "RequestApproval": return request_approval(RequestApprovalArgs(**a))
return ToolReceipt(tool=n, ok=False, ref="none", message="Unknown tool")
# --- Model shim returning a plan per contract (replace with your LLM call) ---
def call_model(contract_yaml: str, claims: List[Dict[str,Any]], invoice: Dict[str,Any]) -> Dict[str,Any]:
return {
"summary": f"Invoice from {invoice['vendor']} approved for payment within company policy.",
"decision": "approve",
"invoice_details": {
"invoice_id": invoice["invoice_id"],
"vendor": invoice["vendor"],
"amount_cents": invoice["amount_cents"],
"due_date": invoice["due_date"].isoformat(),
"payment_method": invoice["payment_method"]
},
"citations": ["policy:invoice:payment_cap", "policy:invoice:payment_method"],
"next_steps": ["Check policy", "Check approval", "Authorize payment"],
"tool_proposals": [
{"name":"CheckInvoicePolicy",
"args":{"invoice_id":invoice["invoice_id"],"amount_cents":invoice["amount_cents"],
"vendor":invoice["vendor"],"payment_method":invoice["payment_method"],
"due_date":invoice["due_date"]},
"preconditions":"Check for policy compliance",
"idempotency_key": new_idem()},
{"name":"CheckApproval",
"args":{"invoice_id":invoice["invoice_id"],"amount_cents":invoice["amount_cents"],
"vendor":invoice["vendor"],"approver_id":"M001"},
"preconditions":"Verify approval status",
"idempotency_key": new_idem()},
{"name":"AuthorizePayment",
"args":{"invoice_id":invoice["invoice_id"],"amount_cents":invoice["amount_cents"],
"payment_method":invoice["payment_method"],"due_date":invoice["due_date"]},
"preconditions":"Authorize payment",
"idempotency_key": new_idem()}
]
}
def render_response(model_json: Dict[str,Any], receipts: List[ToolReceipt]) -> str:
idx = {r.tool:r for r in receipts}
lines = [model_json["summary"], ""]
lines.append(f"Decision: {model_json['decision']}")
if idx.get("AuthorizePayment") and idx["AuthorizePayment"].ok:
amt = idx["AuthorizePayment"].data["amount_cents"] / 100
lines.append(f"Payment authorized: ${amt:.2f} (ref {idx['AuthorizePayment'].ref})")
lines.append("")
lines.append("Next:")
for s in model_json["next_steps"]:
lines.append(f"• {s}")
lines.append("\nCitations: " + ", ".join(model_json["citations"]))
return "\n".join(lines)
def handle(invoice: Dict[str,Any]) -> str:
contract = open("contracts/invoice_approval_v1.yaml").read()
claims: List[Dict[str,Any]] = [] # load real claims
plan = call_model(contract, claims, invoice)
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 {"AuthorizePayment"}:
break
return render_response(plan, receipts)
if __name__ == "__main__":
example_invoice = {
"invoice_id":"inv-890",
"vendor":"Acme Inc",
"amount_cents":95000,
"due_date": date(2025,10,15),
"payment_method":"bank_transfer"
}
print(handle(example_invoice))
The Prompt You’d Send to the Model (concise and testable)
System:
You are InvoiceApprovalAgent. Follow the contract:
- Ask once if invoice_id, amount, vendor, due_date, or payment_method is 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, decision, invoice_details, citations[], next_steps[], tool_proposals[].
Claims (eligible only):
[ ... JSON array of policy claims like above ... ]
User:
Please approve and process this invoice:
{"invoice_id":"inv-890","vendor":"Acme Inc","amount_cents":95000,"due_date":"2025-10-15","payment_method":"bank_transfer"}
How to adapt quickly
Replace the demo adapters with your invoice management and approval systems; keep idempotency for transaction security. Load policy claims from your finance and compliance systems, particularly payment method rules and approval hierarchies. Add a validation layer to check for schema, lexicon, localization, and compliance. Ship the contract, policy bundle, and decoder settings with a feature flag for quick testing, canary deployment, and rollback support.