Controlled Agentic Workflows for Due Diligence: Traceable Automation in Regulated Professional Services (Part 3)

Part 3 applies the architecture to M&A AI automation, contract re-evaluation pipelines, and SoW/KYC agent workflows in audit and compliance, closing with concrete audit trail KPIs technical leads can ship.

From architecture to concrete workflows

Parts 1 and 2 defined why traceable automation matters and what an audit-trail-first architecture looks like.

Now we translate those ideas into concrete workflows across four professional service contexts: M&A AI automation for legal due diligence, contract re-evaluation pipelines for ongoing compliance, SoW/KYC agent workflows in banking, and audit/compliance investigations.

M&A AI automation has already moved well past simple document summarisation. Deal teams now report roughly 40–45% efficiency gains in the analytical, document-heavy front end of transactions ; target screening, contract analysis, and diligence sprints ; even as judgment-intensive negotiation and integration work remains firmly human-led. The pattern holds across legal, audit, and compliance more broadly: agentic AI accelerates evidence collection, risk analysis, and drafting, while building richer, more granular audit trails than manual processes could ever produce.  

The goal is not to remove human judgment but to turn partners into reviewers of well-structured AI reasoning instead of primary producers of first drafts ; and to make every one of those AI-assisted judgments defensible after the fact. That defensibility depends on two things this part will make concrete: workflow-specific implementation patterns (M&A, contract re-evaluation, SoW/KYC, audit investigations) and a shared set of audit trail KPIs that let technical leads and risk committees measure whether the audit-trail-first design is actually working in production.

We’ll cover four patterns:

  • M&A legal due diligence
  • Contract intelligence for continuous compliance
  • SoW / KYC due diligence
  • Audit and compliance investigations

Pattern 1: M&A legal due diligence

Goal: Generate an AI-assisted issue list and draft report over a data room, with partner-verifiable reasoning and full audit trails.

Step 1 – Case intake and context

When a new deal engagement is created, capture a rich case context and bind it to all subsequent agent activity:

const ctx: AgentContext = { 

  caseId: "DD-2026-042", 

  userId: "usr_partner_01", 

  userRole: "PARTNER", 

  agentId: "ag_contract_risk_v3", 

  agentVersion: "3.2.1", 

  policyBundleId: "MNA_EU_US_2026_01" 

};

Intake systems in legal and M&A tools store this metadata, including jurisdiction and regulatory regimes, to drive routing and policy selection.

Step 2 – Evidence ingestion and indexing (with document provenance tracking)

Agents ingest documents from a data room, run OCR, and index them for semantic and keyword search but before any of that content becomes evidence an AI agent reasons over, the ingestion step must establish document provenance: proof that a specific byte-for-byte artifact entered the system at a specific time, plus enough spatial context to point a partner at the exact location on the page a clause was extracted from.

Security guidance for RAG and document pipelines is explicit on this point: hash every document at ingestion time (SHA-256 minimum), store the hash alongside metadata, and reject or flag anything that doesn't match on later retrieval ; this is what lets you later prove a contract wasn't silently swapped or altered between upload and the AI's risk scoring. Layout-aware document AI systems go a step further, capturing not just text but the bounding‑box coordinates of every word, line, and paragraph on the page, so any downstream extraction can be traced back to its exact spatial location rather than just "somewhere in this 80-page contract."

import hashlib 

from dataclasses import dataclass 

@dataclass 

class BoundingBox: 

    page: int 

    x0: float 

    y0: float 

    x1: float 

    y1: float 

def sha256_of_file(path: str) -> str: 

    h = hashlib.sha256() 

    with open(path, "rb") as f: 

        for chunk in iter(lambda: f.read(8192), b""): 

            h.update(chunk) 

    return h.hexdigest() 

def ingest_document( 

    case_id: str, 

    path: str, 

    audit: AuditLog, 

    ocr_layout_fn,  # returns list[dict] with text + BoundingBox per element 

) -> str: 

    doc_id = str(uuid.uuid4()) 

    doc_checksum = sha256_of_file(path)  # provenance anchor 

    layout_elements = ocr_layout_fn(path)  # OCR + spatial layout extraction 

    spatial_layout = [ 

        { 

            "element_id": str(idx), 

            "text": el["text"], 

            "bbox": el["bbox"],  # {page, x0, y0, x1, y1} 

        } 

        for idx, el in enumerate(layout_elements) 

    ] 

    payload = { 

        "path": path, 

        "doc_id": doc_id, 

        "doc_checksum_sha256": doc_checksum, 

        "spatial_layout": spatial_layout, 

    } 

    audit.append( 

        case_id=case_id, 

        agent_id="ag_ingest_v1", 

        actor_id="sys_ingestion_worker", 

        event_type="DOC_INGESTED", 

        payload=payload, 

    ) 

    return doc_id

Two fields in this DOC_INGESTED event are non-negotiable for provenance:

  • doc_checksum_sha256 – A SHA-256 hash of the raw file computed at ingestion time. This lets you verify at any later point including during litigation or a regulator's inspection that the document an agent scored is byte-identical to the document that was actually uploaded to the data room, and lets a retrieval or review step reject a document whose hash no longer matches.
  • spatial_layout – Bounding-box coordinates (page, x0/y0/x1/y1) for each extracted text element, following the same layout-aware pattern used by document-understanding models like LayoutLM and Azure Document Intelligence. This is what lets your evidence_refs in the reasoning schema (Part 2) point not just to a doc_id and page, but to the exact bounding box a clause was extracted from, so a partner reviewing a risk score can jump directly to the highlighted region on the source page instead of re-reading the whole document.

This pattern mirrors SoW due diligence systems, where each document is indexed with explicit provenance and anchored into the audit trail, and extends it with the checksum + spatial metadata that production document-AI pipelines increasingly treat as mandatory rather than optional.

Step 3 – Clause extraction and risk scoring

Using the reasoning schema from Part 2, implement an extraction agent that calls an LLM and logs structured risk assessments:

def score_clause( 

    case_id: str, 

    clause_text: str, 

    evidence_ref: dict, 

    audit: AuditLog, 

    reasoning_model, 

) -> dict: 

    # Call LLM to get structured reasoning (pseudo-code) 

    reasoning = reasoning_model(clause_text, evidence_ref) 

  

    validate_contract_risk(reasoning) 

    audit.append( 

        case_id=case_id, 

        agent_id="ag_contract_risk_v3", 

        actor_id="usr_associate_01", 

        event_type="CLAUSE_RISK_SCORED", 

        payload={"reasoning": reasoning}, 

    ) 

    return reasoning

Contract AI and due diligence tools emphasise that every extracted datapoint and risk assessment should be traceable back to source clauses and reasoning.

Pattern 2: Contract intelligence for ongoing compliance

Goal: When policies change, re-evaluate existing contracts and surface cases with structured reasoning and partner approval trails.

Policy update and propagation

// Define a versioned policy contract. 
interface PolicyVersion { 
  id: string; 
  name: string; 
  effectiveFrom: string; 
  rules: any; // Replace with your DSL, OPA/Rego, or JSON rules type. 
} 

// Create the new policy version to apply during re-evaluation. 
const newPolicy: PolicyVersion = { 
  id: "pol_residency_v2", 
  name: "Data Residency Policy v2", 
  effectiveFrom: "2026-07-01T00:00:00Z", 
  rules: { 
    // Define residency requirements, exceptions, and review triggers here. 
  }, 
}; 

Policy engines for AI agents often use versioned bundles, and audit logs must record which version was in force for each decision.

Architectural consideration: asynchronous parallel processing

A policy update rarely affects one contract : it typically triggers re-evaluation across a repository of thousands of historical agreements. Running that re-evaluation synchronously inside the request/response cycle that fired the policy update is a non-starter: it would block on LLM calls and document retrieval for hours, has no resumability if a worker crashes midway through contract #4,200, and gives you no backpressure control when the risk model or vector store gets overloaded.

The correct pattern is to decouple triggering from execution using event queues and let a fleet of stateless batch workers pull work independently. Two common architectures fit this:

  • Celery + RabbitMQ – The policy engine publishes one task message per contract (or per shard of contracts) to a durable RabbitMQ queue; a pool of Celery workers consumes messages, calls evaluateContractAgainstPolicy, and acknowledges only after the audit event is durably written, so a crashed worker causes RabbitMQ to redeliver the message rather than silently drop it.
  • Temporal – Each contract re-evaluation becomes a Workflow Execution (or a batch of contracts becomes a Child Workflow under a Fan-Out pattern), giving you durable execution: state is checkpointed automatically, failures retry with exponential backoff, and you can query exactly which contracts completed, failed, or are still in flight without hand-rolled retry logic.

Either way, the key architectural rule is that each batch worker pushes its own state events back to the central AuditSink as it progresses : not just a final success/failure signal. That means every worker emits CONTRACT_POLICY_EVAL_STARTED, intermediate CONTRACT_POLICY_EVAL_RETRY events on transient failures, and a terminal CONTRACT_POLICY_EVAL_COMPLETED/FAILED event, all correlated to the same policyBundleId and a shared batchRunId so a partner or auditor can reconstruct the full sweep: including partial failures and retries : months later, not just a summary count of "3,412 contracts processed."

// worker.ts — one Celery/Temporal activity per contract 

async function reevaluationWorker( 

  ctx: AgentContext, 

  contractId: string, 

  policy: PolicyVersion, 

  batchRunId: string, 

  sink: AuditSink 

) { 

  const workerCtx = { ...ctx, policyBundleId: policy.id }; 

  await sink.append({ 

    eventId: randomUUID(), 

    occurredAt: new Date().toISOString(), 

    context: workerCtx, 

    eventType: "CONTRACT_POLICY_EVAL_STARTED", 

    inputSummary: JSON.stringify({ contractId, batchRunId }), 

    outputSummary: "", 

  } as any); 

  

  try { 

    const result = await evaluateContractAgainstPolicy(workerCtx, contractId, policy, sink); 

    await sink.append({ 

      eventId: randomUUID(), 

      occurredAt: new Date().toISOString(), 

      context: workerCtx, 

      eventType: "CONTRACT_POLICY_EVAL_COMPLETED", 

      inputSummary: JSON.stringify({ contractId, batchRunId }), 

      outputSummary: JSON.stringify(result), 

    } as any); 

    return result; 

  } catch (err) { 

    // Celery/Temporal retry policy handles re-delivery; 

    // this event just makes the retry visible in the audit trail 

    await sink.append({ 

      eventId: randomUUID(), 

      occurredAt: new Date().toISOString(), 

      context: workerCtx, 

      eventType: "CONTRACT_POLICY_EVAL_RETRY", 

      inputSummary: JSON.stringify({ contractId, batchRunId, error: String(err) }), 

      outputSummary: "", 

    } as any); 

    throw err; // re-throw so the queue/workflow engine handles redelivery 

  } 

}

Re-evaluation and logging

async function evaluateContractAgainstPolicy( 

  ctx: AgentContext, 

  contractId: string, 

  policy: PolicyVersion, 

  sink: AuditSink 

) { 

  const wrapped = withAuditTrail( 

    { ...ctx, policyBundleId: policy.id }, 

    sink, 

    "CONTRACT_POLICY_EVAL", 

    async () => { 

      // pseudo: fetch clauses & run risk model 

      const reasoning: ReasoningArtifact = await runRiskModel(contractId, policy); 

      const result = { contractId, policyId: policy.id, recommendation: "REVIEW" }; 

      return { result, reasoning }; 

    } 

  ); 

  

  return wrapped(); 

}

This pattern matches real implementations where policy changes trigger batch re-evaluation of contracts and generation of structured audit records.

Pattern 3: SoW / KYC due diligence in banking

The SoW agentic blueprint includes multiple agents (identification, corroboration, gap analysis, plausibility, narrative) coordinating over an evidence index.

Here’s how you might log gap analysis and narrative generation.

Gap analysis agent

def gap_analysis( 

    case_id: str, 

    evidence_index: list[dict], 

    expected_categories: list[str], 

    audit: AuditLog, 

) -> dict: 

    present = {e["category"] for e in evidence_index} 

    missing = [c for c in expected_categories if c not in present] 

  

    reasoning = { 

        "schema_version": "sowGap.v1", 

        "missing_categories": missing, 

        "evidence_refs": evidence_index, 

    } 

  

    audit.append( 

        case_id=case_id, 

        agent_id="ag_sow_gap_v1", 

        actor_id="sys_sow_orchestrator", 

        event_type="SOW_GAP_ANALYZED", 

        payload={"reasoning": reasoning}, 

    ) 

    return reasoning

The published SoW agent system logs multi-phase reasoning, including gap analysis, in a structured way to support later replay and evaluation.

Narrative agent with evidence-linked explanation

def sow_narrative( 

    case_id: str, 

    corroboration: dict, 

    gaps: dict, 

    audit: AuditLog, 

    llm_client, 

) -> str: 

    prompt = { 

        "corroboration": corroboration, 

        "gaps": gaps, 

    } 

    # LLM sees structured data, not raw docs 

    narrative = llm_client.generate_sow_summary(prompt) 

  

    audit.append( 

        case_id=case_id, 

        agent_id="ag_sow_narrative_v1", 

        actor_id="usr_compliance_analyst_01", 

        event_type="SOW_NARRATIVE_DRAFTED", 

        payload={ 

            "input_summary": prompt, 

            "narrative": narrative[:2000], 

        }, 

    ) 

    return narrative

The SoW paper emphasises evidence-linked narratives where each assertion can be traced back to specific documents and corroboration steps.

Pattern 4: Audit and compliance investigations

Transactions, logs, and external feeds yield alerts which agentic workflows can triage and investigate.

Design guides for AI agent logging stress that each step—evidence pull, policy evaluation, recommendation, human override—must be logged for audit.

Investigation record shape

interface InvestigationEvent { 

  eventId: string; 

  occurredAt: string; 

  caseId: string; 

  alertId: string; 

  stage: 

    | "INTAKE" 

    | "EVIDENCE_GATHERING" 

    | "RISK_SCORING" 

    | "DECISION" 

    | "HUMAN_REVIEW"; 

  actorId: string; 

  actorType: "AGENT" | "HUMAN"; 

  details: Record<string, unknown>; 

} 

Data privacy and redaction: sanitize before you hash

Investigation logs are the highest-risk log category in this whole architecture, because details in the interface above almost inevitably ends up holding raw API payloads, transaction records, KYC documents, hotline transcripts , that contain PII or PHI. Writing that raw content directly into a hash-chained, append-only ledger is a serious compliance mistake: unlike a normal database row, you generally cannot delete or edit a chained audit record without breaking every subsequent hash, so any PII that lands in the chain is effectively permanent, defeating GDPR erasure rights and HIPAA minimum-necessary requirements by design.

The fix is to insert an automated PII/PHI Redaction Sanitizer as a mandatory middleware step  before hashing, not after so sensitive values never enter the cryptographic chain in the first place. This must happen at the point of capture (the logging SDK or an inline gateway), not as a downstream cleanup pass, because once raw content is durably persisted, redacting it later doesn't undo the exposure.

Three redaction strategies, and when to use each:

  • Redact – Replace the value entirely (e.g., [REDACTED]) for high-sensitivity fields with no debugging or correlation value, such as SSNs or full account numbers.
  • Mask – Show a partial value (e.g., last four digits) when a reviewer needs enough context to recognize the record without seeing the full sensitive value.
  • Deterministic hash (salted/keyed) – Convert a value to a consistent SHA-256 (or HMAC) digest when you need to correlate the same entity across events without ever storing or revealing the underlying value  critical for linking multiple investigation events to the same customer without persisting their PII.
// piiSanitizer.ts 

import { createHash, createHmac } from "crypto"; 

const SENSITIVE_KEYS = new Set([ 

  "ssn", "passport_number", "account_number", "password", "api_key", 

  "date_of_birth", "email", "phone", "full_name", 

]); 

const EMAIL_PATTERN = /[\w.+-]+@[\w-]+\.[\w.-]+/g; 

const HASH_SALT = process.env.PII_HASH_SALT ?? ""; // required in production 

function hmacDigest(value: string): string { 

  return "hmac:" + createHmac("sha256", HASH_SALT).update(value).digest("hex").slice(0, 16); 

} 

export function sanitizePayload(payload: Record<string, unknown>): Record<string, unknown> { 

  const clean: Record<string, unknown> = {}; 

  for (const [key, value] of Object.entries(payload)) { 

    if (SENSITIVE_KEYS.has(key.toLowerCase())) { 

      clean[key] = typeof value === "string" ? hmacDigest(value) : "[REDACTED]"; 

      continue; 

    } 

    if (typeof value === "string") { 

      clean[key] = value.replace(EMAIL_PATTERN, (m) => hmacDigest(m)); 

    } else if (typeof value === "object" && value !== null) { 

      clean[key] = sanitizePayload(value as Record<string, unknown>); 

    } else { 

      clean[key] = value; 

    } 

  } 

  return clean; 

} 

 

// auditSink.ts — sanitize is the mandatory first step, hashing is the second 

export async function appendInvestigationEvent( 

  sink: AuditSink, 

  raw: InvestigationEvent 

) { 

  const sanitized: InvestigationEvent = { 

    ...raw, 

    details: sanitizePayload(raw.details), // PII/PHI stripped BEFORE hashing 

  }; 

  // chain_hash / content_hash is computed over `sanitized`, never over `raw` 

  await sink.append(sanitized as any); 

}

This ordering matters mechanically, not just procedurally: because the hash chain is computed over whatever payload is actually passed to append(), running the sanitizer first means the chain's cryptographic integrity is computed over the redacted, compliant payload verifying the chain later proves the sanitized record hasn't been altered, without ever requiring the original raw PII to exist in the ledger at all. For AI-gateway-style architectures, this same pattern is described as writing a "content-free" hash-chained audit log  the log proves an event happened and that its content hasn't changed since, without the ledger ever holding the sensitive content itself. Keep the field-level classification list (SENSITIVE_KEYS) centrally maintained and version it alongside your reasoning and policy schemas, since new PII/PHI field types tend to appear as new data sources are onboarded.

Example: risk scoring step

async function riskScoreAlert( 

  alertId: string, 

  ctx: AgentContext, 

  sink: AuditSink 

) { 

  const eventId = randomUUID(); 

  const occurredAt = new Date().toISOString(); 

  

  const score = await computeRiskScore(alertId); // pseudo 

  const record: InvestigationEvent = { 

    eventId, 

    occurredAt, 

    caseId: ctx.caseId, 

    alertId, 

    stage: "RISK_SCORING", 

    actorId: ctx.agentId, 

    actorType: "AGENT", 

    details: { score } 

  }; 

  

  await sink.append(record as any); 

  return score; 

} 

AML and fraud tools built on agentic workflows log every investigation phase, making it easy to export a case history that regulators can inspect.

Implementation checklist for technical leads

Putting it all together, your due diligence platform should:

  • Bind identity to every agent action – User principal, agent identity, case context, and policy bundle ID.
  • Adopt hash-chained, append-only logs – For tamper-evident audit records with offline verification.
  • Log across layers – Tools, decisions, policies, and reasoning artifacts, not just HTTP requests.
  • Use versioned schemas – For reasoning, policy, and gate outcomes, validated at the boundary.
  • Expose replay and export – Case replay UIs and NDJSON/CSV exports for regulators and internal auditors.
  • Automated log integrity verification and alerting – A hash chain that nobody checks is a false sense of security: tampering could sit undetected for months until an auditor finally asks for a case reconstruction. Technical leads should schedule a background job hourly is a common cadence in production tamper-evident logging systems that walks the entire chain (or the delta since the last run), recomputes every chain_hash, and compares it against the stored value using the same verify_chain() logic from Part 1. The moment a mismatch is found, the job should fire an immediate, out-of-band alert to the SecOps team (Slack, PagerDuty, or your SIEM) rather than waiting for the next manual audit, and should also write an in-band INTEGRITY_CHECK_FAILED event to a separate verification log so the failure itself is durably recorded even if the primary chain is compromised.
#integrity_watchdog.py — run on a scheduler (cron, systemd timer, or Celery beat) 

import logging from datetime import datetime, timezone 

logger = logging.getLogger("audit_integrity_watchdog") 

def run_integrity_check(audit: "AuditLog", alert_sink) -> bool: is_valid = audit.verify_chain() checked_at = datetime.now(timezone.utc).isoformat() 

if not is_valid: 
    alert_sink.page_secops( 
        severity="CRITICAL", 
        message=f"Audit chain integrity check FAILED at {checked_at}. " 
                 f"Hash mismatch detected — possible tampering or data loss.", 
    ) 
    logger.critical("AUDIT_CHAIN_BROKEN checked_at=%s", checked_at) 
else: 
    logger.info("AUDIT_CHAIN_OK checked_at=%s event_count=%d", checked_at, len(audit._events)) 
 
return is_valid 

This closes the loop between Part 1's tamper-evident design and Part 2's identity-bound logging layers: a broken chain is detected in minutes rather than discovered or missed entirely during a regulatory inspection months later

KPIs and success metrics

To demonstrate value and safety to partners and risk committees, track:

  • Reduction in time-to-first-draft for due diligence reports or investigations, while maintaining or improving issue coverage.
  • Percentage of agent decisions that pass partner or auditor review without modification (decision quality).
  • Percentage of decisions with fully evidence-linked reasoning artifacts in the audit ledger (reasoning completeness).
  • Mean time to respond to regulatory or internal audit requests for case reconstruction (audit readiness).
  • Number of detected policy violations or access anomalies via audit-trail monitoring (governance effectiveness).
  • Schema Compliance Rate – The percentage of agent outputs that pass reasoning-schema validation on the first attempt, before any Structured Repair Loop correction. Structured-output benchmarks distinguish this cleanly: a "schema pass rate" or "first-attempt acceptance rate" measured as schema-valid responses divided by total requests, tracked separately from the final acceptance rate after retries. This matters because near-perfect schema pass rates can mask a high retry burden frontier models routinely clear 95%+ JSON/schema validity, but the gap between first-pass and final-pass compliance is exactly what your REASONING_VALIDATION_RETRY audit events (from the repair-loop pattern) let you quantify per agent, per policy version, per clause type. A declining first-pass rate after a model or prompt change is an early warning sign worth alerting on before it becomes a partner-facing quality issue.
  • Traceability Coverage – The percentage of assertions in a generated report that link back to a verified document offset ID (the doc_id + page + bbox/offset evidence reference from the reasoning schema). The target is 100%: every claim a partner reads in an issue list or SoW narrative should be clickable back to the exact source location it was extracted from, with no "orphan" assertions that exist in the narrative but have no corresponding evidence_refs entry. This is distinct from Schema Compliance Rate a reasoning artifact can be perfectly schema-valid (right shape, right types) while still containing an ungrounded or hallucinated value that has no real link to the source document, a failure mode structured-output research explicitly documents as passing shape checks while failing on value grounding. Traceability Coverage should be computed as a batch metric across every case (count of grounded assertions ÷ total assertions) and reported alongside Schema Compliance Rate, since a system can score well on one and poorly on the other.

Why report these two together: Schema Compliance Rate catches whether the agent's output has the right shape; Traceability Coverage catches whether the content inside that shape is actually anchored to real evidence. Structured-output research shows these are genuinely separate failure modes a model can produce a syntactically perfect, schema-valid JSON object that still contains a value with no grounding in the source document, and schema validators alone won't catch it. Tracking both closes that blind spot and gives risk committees a metric pair that maps directly to "is the AI's output well-formed" versus "is the AI's output actually true to the evidence."

Closing: designing for trust, not just throughput

Controlled agentic workflows for due diligence are about turning AI into a traceable collaborator whose actions and reasoning can be inspected, challenged, and defended.

By combining structured reasoning schemas, hash-chained audit logs, identity-bound events, and explicit human approval gates, you get automation that meets the expectations of regulators, clients, and your own partners.

If you wire your M&A, contract intelligence, SoW/KYC, and audit workflows with these patterns and code-level practices, you’ll have a platform where an AI agent’s decision trail is as robust and more reconstructable than any human-only process you’ve run before.

- Authored by Sonal Dwevedi & Tharun Mathew