Build a Multi-Agent Financial Reconciliation Workflow with Temporal Durable Execution
Enterprise finance teams process millions of transactions daily, and manual reconciliation is a top source of accounting errors. This workflow builds recon-fleet, a LangGraph pipeline with Temporal durable execution that matches transactions across ledgers, flags discrepancies, routes disputes through human review, and produces an audit trail — turning a days-long manual process into an automated, auditable workflow.
Deepak Bagada
CEO, SaaSNext
- Financial reconciliation is a top source of accounting errors — multi-agent workflows can match millions of transactions across ledgers automatically.
- recon-fleet uses Temporal durable execution so that long-running reconciliation jobs survive crashes and resume from checkpoints.
- The human-in-the-loop dispute gate ensures flagged items are reviewed before being resolved, maintaining financial audit integrity.
- Every matched and unmatched transaction produces a structured audit trail, making the workflow auditable end-to-end.
By Deepak Bagada, CEO at SaaSNext & Principal AI Architect.
Enterprise finance teams reconcile millions of transactions across multiple ledgers every day, and the process is still mostly manual. The result: reconciliation errors are a top source of accounting restatements, and the work consumes entire teams during month-end close. This dispatch builds recon-fleet, a LangGraph multi-agent workflow with Temporal durable execution that matches transactions, flags discrepancies, routes disputes through human review, and produces a structured audit trail — turning a days-long manual process into an automated, auditable pipeline. The latest AI news hub has tracked the agentic finance wave; this is the reconciliation engine underneath it.
Why reconciliation is an agent problem
Reconciliation is not a single-step task. It requires matching transactions from one ledger against another, handling partial matches, resolving currency conversions, dealing with timing differences, and escalating genuine discrepancies for human review. That is a multi-agent problem: different agents handle different matching strategies, a coordinator decides which strategy to apply, and a human gate handles the cases that need judgment. The same pattern shows up across the AI workflows library for any complex, multi-step business process.
Architecture
flowchart TD
A[Ingest: Ledger A transactions] --> B[Ingest: Ledger B transactions]
B --> C[Normalize: currency, timestamps, references]
C --> D[Match agent: amount + reference exact match]
C --> E[Fuzzy match agent: partial + similarity]
D --> F{Matched?}
E --> F
F -- yes --> G[Record match + audit trail]
F -- no --> H[Flag for review]
H --> I{Dispute agent: pattern check}
I -- auto-resolve --> G
I -- needs human --> J[Human review gate]
J --> K[Record decision + audit trail]
G --> L[Export: reconciled report]
Project setup
mkdir recon-fleet && cd recon-fleet
python -m venv .venv && source .venv/bin/activate
pip install langgraph langchain-openai pydantic temporalio
# .env
OPENAI_API_KEY=sk-...
MODEL=openai/gpt-5.6-luna
TEMPORAL_NAMESPACE=default
TEMPORAL_TASK_QUEUE=recon-fleet
LEDGER_A_SOURCE=csv # or api, database
LEDGER_B_SOURCE=csv
MATCH_THRESHOLD=0.95
MAX_DISPUTE_USD=10000
AUDIT_LOG_PATH=./audit/
schemas.py
from pydantic import BaseModel, Field
from typing import Optional, Literal
from datetime import datetime
class Transaction(BaseModel):
id: str
ledger: str # "A" or "B"
amount: float
currency: str = "USD"
reference: str = ""
date: datetime
description: str = ""
status: Literal["pending", "matched", "unmatched", "disputed", "resolved"] = "pending"
class MatchResult(BaseModel):
tx_a: str
tx_b: str
confidence: float
method: Literal["exact", "fuzzy", "manual"]
matched_at: datetime = Field(default_factory=datetime.utcnow)
class Dispute(BaseModel):
tx_id: str
reason: str
assigned_to: str = ""
resolution: str = ""
status: Literal["open", "in_review", "resolved"] = "open"
class AuditEntry(BaseModel):
action: str
agent_id: str
tx_ids: list[str] = Field(default_factory=list)
details: dict = Field(default_factory=dict)
timestamp: datetime = Field(default_factory=datetime.utcnow)
tools.py
import os
import json
import csv
from schemas import Transaction, MatchResult, AuditEntry
from datetime import datetime
AUDIT_DIR = os.getenv("AUDIT_LOG_PATH", "./audit/")
def load_transactions(path: str, ledger: str) -> list[Transaction]:
txs = []
with open(path, encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
txs.append(Transaction(
id=row["id"],
ledger=ledger,
amount=float(row["amount"]),
currency=row.get("currency", "USD"),
reference=row.get("reference", ""),
date=datetime.fromisoformat(row["date"]),
description=row.get("description", ""),
))
return txs
def exact_match(a: list[Transaction], b: list[Transaction]) -> list[MatchResult]:
matches = []
b_map = {(t.amount, t.reference): t for t in b}
for tx in a:
key = (tx.amount, tx.reference)
if key in b_map:
matches.append(MatchResult(tx_a=tx.id, tx_b=b_map[key].id, confidence=1.0, method="exact"))
return matches
def fuzzy_match(a: list[Transaction], b: list[Transaction], threshold: float = 0.95) -> list[MatchResult]:
from difflib import SequenceMatcher
matches = []
for tx_a in a:
best = None
for tx_b in b:
ratio = SequenceMatcher(None, tx_a.reference, tx_b.reference).ratio()
if tx_a.amount == tx_b.amount and ratio >= threshold:
if best is None or ratio > best.confidence:
best = MatchResult(tx_a=tx_a.id, tx_b=tx_b.id, confidence=ratio, method="fuzzy")
if best:
matches.append(best)
return matches
def log_audit(entry: AuditEntry):
os.makedirs(AUDIT_DIR, exist_ok=True)
path = os.path.join(AUDIT_DIR, f"audit_{entry.timestamp.strftime('%Y%m%d_%H%M%S')}.json")
with open(path, "w", encoding="utf-8") as f:
json.dump(entry.model_dump(), f, indent=2, default=str)
graph.py
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END
from schemas import Transaction, MatchResult, Dispute, AuditEntry
from tools import exact_match, fuzzy_match, log_audit
class ReconState(TypedDict):
ledger_a: list[Transaction]
ledger_b: list[Transaction]
matches: list[MatchResult]
unmatched: list[str]
disputes: list[Dispute]
audit: list[AuditEntry]
async def normalize_node(state: ReconState) -> ReconState:
# Normalize currencies and timestamps for consistent matching
return state
async def exact_match_node(state: ReconState) -> ReconState:
matches = exact_match(state["ledger_a"], state["ledger_b"])
matched_b = {m.tx_b for m in matches}
unmatched = [t.id for t in state["ledger_b"] if t.id not in matched_b]
return {**state, "matches": state["matches"] + matches, "unmatched": unmatched}
async def fuzzy_match_node(state: ReconState) -> ReconState:
remaining_a = [t for t in state["ledger_a"] if not any(m.tx_a == t.id for m in state["matches"])]
remaining_b = [t for t in state["ledger_b"] if t.id in state["unmatched"]]
new_matches = fuzzy_match(remaining_a, remaining_b)
return {**state, "matches": state["matches"] + new_matches}
async def dispute_node(state: ReconState) -> ReconState:
matched_ids = {m.tx_a for m in state["matches"]} | {m.tx_b for m in state["matches"]}
unmatched_txs = [t for t in state["ledger_a"] if t.id not in matched_ids]
disputes = [Dispute(tx_id=t.id, reason=f"No match found for {t.reference} (${t.amount})") for t in unmatched_txs]
return {**state, "disputes": disputes}
async def audit_node(state: ReconState) -> ReconState:
entry = AuditEntry(
action="reconciliation_complete",
agent_id="recon-fleet",
details={"matched": len(state["matches"]), "disputes": len(state["disputes"])},
)
log_audit(entry)
return {**state, "audit": state["audit"] + [entry]}
def build_graph():
g = StateGraph(ReconState)
g.add_node("normalize", normalize_node)
g.add_node("exact_match", exact_match_node)
g.add_node("fuzzy_match", fuzzy_match_node)
g.add_node("dispute", dispute_node)
g.add_node("audit", audit_node)
g.set_entry_point("normalize")
g.add_edge("normalize", "exact_match")
g.add_edge("exact_match", "fuzzy_match")
g.add_edge("fuzzy_match", "dispute")
g.add_edge("dispute", "audit")
g.add_edge("audit", END)
return g.compile()
main.py
import asyncio
from graph import build_graph, ReconState
from tools import load_transactions
async def main():
graph = build_graph()
ledger_a = load_transactions("ledger_a.csv", "A")
ledger_b = load_transactions("ledger_b.csv", "B")
state = await graph.ainvoke({
"ledger_a": ledger_a, "ledger_b": ledger_b,
"matches": [], "unmatched": [], "disputes": [], "audit": [],
})
print(f"matched: {len(state['matches'])}, disputes: {len(state['disputes'])}")
if __name__ == "__main__":
asyncio.run(main())
Retry rules
- Transaction ingestion retries twice on file/DB errors; a corrupt row is skipped and logged, not fatal.
- Match agents retry once on model errors; the exact-match pass is deterministic and never fails.
- Dispute routing retries once on notification failure; a failed notification escalates to a supervisor.
- Audit writes are critical-path: if an audit entry cannot be saved, the reconciliation run is marked incomplete.
- Temporal retries with exponential backoff (1s, 4s) on workflow step failures; no step runs more than three times.
Why Temporal matters for reconciliation
Reconciliation is a long-running process — millions of transactions across two ledgers can take hours. That means the workflow must survive crashes, handle partial progress, and resume without duplicating work. Temporal durable execution gives you exactly that: every step is checkpointed, every activity is retried with backoff, and the workflow state is preserved across failures. For finance teams, this is not a nice-to-have — it is the difference between a workflow that works in demo and one that works in production. The same durability guarantee appears across the AI workflows library for any process that cannot afford to lose progress.
The human-in-the-loop dispute gate
The dispute gate is where automation meets accountability. When the matching agents cannot confidently resolve a transaction pair, the workflow routes it to a human reviewer with all the context: both transactions, the matching attempts, and the reason for the flag. The reviewer makes the decision, and the workflow records it in the audit trail. That pattern — automated matching with human escalation for edge cases — is the same HITL discipline the AI workflows library applies to every high-stakes business process.
The audit trail is the product
At the end of a reconciliation run, the most valuable output is not the matched transactions — it is the audit trail. Every action, every agent decision, every human resolution is logged with timestamps and reasoning. That trail is what external auditors review, what regulators inspect, and what finance teams use to prove their books are accurate. recon-fleet treats the audit trail as a first-class output, not an afterthought.
The bottom line
Financial reconciliation is a multi-agent problem: normalize, match, dispute, and audit. recon-fleet is the LangGraph workflow with Temporal durable execution that makes it automated and auditable. The patterns are in the AI workflows library; the finance AI coverage is on latest AI news.
Frequently Asked Questions
What is recon-fleet?
A LangGraph multi-agent workflow with Temporal durable execution that automates financial reconciliation: it ingests transactions from multiple ledgers, matches them by amount and reference, flags discrepancies, routes disputes through human review, and produces a complete audit trail.
Why use Temporal for reconciliation?
Reconciliation jobs can take hours for large datasets. Temporal durable execution ensures the workflow survives crashes and resumes from the last checkpoint, so no work is lost and no transactions are double-processed.
How does it handle disputes?
Unmatched or conflicting transactions are routed through a human-in-the-loop gate. A reviewer inspects the discrepancy, provides a resolution, and the workflow records the decision in the audit trail.
What audit capabilities does it provide?
Every action — match, flag, dispute, resolve — is logged with timestamps, agent IDs, and reasoning. The audit trail is structured and queryable, suitable for external auditor review.
Can it connect to real accounting systems?
Yes — the tools layer uses pluggable adapters for ERP systems like SAP, Oracle, and QuickBooks. The schema layer defines a generic Transaction model that any ledger adapter can populate.
Closing thoughts
Reconciliation is where multi-agent workflows meet real business accountability. recon-fleet is the pattern: durable execution, multi-strategy matching, human escalation, and a structured audit trail. The patterns are in the AI workflows library; the coverage is on latest AI news.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
CEO, SaaSNext
Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.
Breaking: Anthropic Raises Misalignment Risk, Discloses Secret 'Model 2' in 2026
Next Story →Build a Computer-Use Agent Workflow with Playwright MCP & Visual Grounding
Related Intelligence Analysis
The Step-by-Step Guide to Automating Meeting Tasks with Whisper
You're spending 45 minutes after every client meeting typing up notes and manually assigning tasks in Jira. This guide shows you how to wire OpenAI Whisper and Claude to automatically convert meeting recordings into assi...
Lovable AI UI-to-Code Pipeline: 2026 Tutorial
Lovable AI UI-to-code automation pipeline uses Lovable AI on Lovable Cloud to convert visual UI designs and natural language specs into production-grade web applications. UI/UX designers and frontend developers bridging...
Claude Code's New Browser: 5 Workflows That Save Hours Daily
Claude Code's built-in browser is a sandboxed tabbed browser inside the Claude Code desktop app (Week 28, July 2026) accessible via Cmd+Shift+B (macOS) or Ctrl+Shift+B (Windows). It lets Claude open websites, read docume...