GraphSentinel- Agentic fraud investigation A developer built GraphSentinel, an agentic fraud-investigation and next-best-action system for the TigerGraph Agentic Fraud Investigation challenge. The system combines a graph store for structured evidence, a risk model for fraud probability, a policy engine for permissions and approvals, and a LangGraph state machine that selects follow-up queries using value-of-information scoring and records a next-best action before and after requesting additional evidence. It supports local graph, TigerGraph REST, and TigerGraph MCP backends under a shared query contract. Fraud investigation is not only a classification problem. An analyst needs to understand why a transaction is risky, gather the right evidence, follow policy, choose an action, document approvals, and preserve the investigation for the next case. GraphSentinel was built to explore that complete workflow. It is an agentic fraud-investigation and next-best-action system for the TigerGraph Agentic Fraud Investigation challenge. The system starts from a risk alert, customer report, or analyst request and produces a traceable investigation record containing graph evidence, model belief, policy citations, evidence requests, actions, approvals, SAR decisions, and case memory. The central idea is: Use the graph to gather structured evidence, use a model to estimate risk, use policy to constrain actions, and use an agent to decide what investigation should happen next. GraphSentinel combines five main ideas: The interesting part is that the workflow records a next-best action before requesting additional evidence . If the case is still uncertain, the system chooses an allowed evidence request using value-of-information scoring. After the response, it updates its belief and records another next-best action. This makes the effect of evidence visible instead of hiding everything inside a final classification. The application supports: Local graph store │ ├── Offline development │ ├── TigerGraph REST │ └── TigerGraph MCP The local graph implementation follows the same query contract as the TigerGraph backends, allowing the investigation workflow to be tested without requiring a live TigerGraph instance. At a high level, an investigation follows this path: Trigger │ ▼ Intake │ ▼ Baseline Graph Evidence │ ▼ Agent-selected Follow-up Queries │ ├───────────────┐ ▼ ▼ Similar Cases GraphRAG │ │ └───────┬───────┘ ▼ Risk Model + Fraud Classification │ ▼ Policy Decision │ ▼ NBA Before Evidence │ ▼ Is the case uncertain? / \ No Yes │ │ │ ▼ │ Select Evidence │ │ │ ▼ │ Apply Response │ │ │ ▼ │ Update Belief │ │ │ ▼ │ NBA After Evidence │ │ └──────┬───────┘ ▼ Actions / Approvals / SAR │ ▼ Explanation │ ▼ Graph Write-back The main runtime is assembled by services/runtime.py . It loads the dataset, graph store, policy configuration, pattern library, risk model, likelihood tables, case repository, evidence provider, and optional CrewAI client. The agent/workflow.py module builds the LangGraph state machine with explicit nodes for: intake baseline evidence follow-ups memory / RAG assessment decision evidence finalization This explicit state-machine approach makes the investigation path easier to test and reason about than putting the entire workflow inside a single agent prompt. One of the most important architectural decisions was deliberately separating responsibilities. ┌───────────────────────────┐ │ TigerGraph │ │ │ │ Evidence + Relationships │ └─────────────┬─────────────┘ │ ▼ ┌───────────────────────────┐ │ Risk Model │ │ │ │ Fraud probability │ └─────────────┬─────────────┘ │ ▼ ┌───────────────────────────┐ │ Policy Engine │ │ │ │ Permissions + Approvals │ └─────────────┬─────────────┘ │ ▼ ┌───────────────────────────┐ │ Agent / LLM │ │ │ │ Follow-up + Explanation │ └─────────────┬─────────────┘ │ ▼ ┌───────────────────────────┐ │ Action Gateway │ │ │ │ Approved actions only │ └───────────────────────────┘ The graph supplies evidence. The risk model estimates fraud probability. The policy engine determines permissions and approval routes. The LLM proposes optional follow-up work and generates language. The action gateway executes only policy-approved actions. The LLM is never allowed to authorize or execute a protective action . Its JSON output is validated, unknown tool names are discarded, citations are filtered against retrieved policy clauses, and failures fall back to deterministic templates. Every investigation begins with a focal transaction. The system gathers a bounded set of temporal graph queries: Transaction details + owner │ ├── Customer history ├── Card activity ├── Shared devices ├── Address peers ├── Customer case history └── Linked cases Representative queries include: gs txn detail gs customer history gs device customers gs address peers gs linked cases These queries are transformed into signals such as: signals = { "amount ratio": amount ratio, "device novelty": device novelty, "account age": account age, "card velocity": card velocity, "email mismatch": email mismatch, "linked confirmed cases": linked confirmed cases, "graph fraud proximity": fraud proximity, "address cluster size": address cluster size, } A critical constraint is that behavioral queries are evaluated strictly before the focal event . If a transaction occurred on January 10, information that only became available on January 20 cannot influence the January 10 decision. A simplified interface therefore looks like: python def get customer history customer id: str, as of: datetime, : return graph.run query "gs customer history", { "customer id": customer id, "as of": as of.isoformat , }, This as of boundary is part of the graph-access layer rather than an assumption made by the analyst. Baseline evidence is not always enough. The agent can select additional graph investigations, for example: Device ring expansion Card activity Address-cluster transactions Community statistics Spending trajectory The agent can select up to three additional tools. CrewAI acts as a bounded investigation assistant for this stage. It receives the available signals and a menu of installed tools and returns structured output such as: { "tools": { "name": "device ring", "reason": "Device is shared with multiple high-risk customers." }, { "name": "address cluster", "reason": "Several recently created accounts share this address." } } The important part is that the model doesn't receive arbitrary database access. The returned tool names are validated against the installed tool registry: TOOLS = { "device ring": investigate device ring, "card activity": investigate card activity, "address cluster": investigate address cluster, "community stats": investigate community, "spending trajectory": investigate spending, } def validate tools requested : return tool for tool in requested if tool "name" in TOOLS Unknown tools are discarded. If the model produces malformed output or fails completely, the workflow falls back to deterministic planning. This creates a controlled boundary: LLM │ │ proposes ▼ Tool Registry │ │ validates ▼ Installed Graph Queries rather than: LLM ─────────────► arbitrary database access Fraud investigation requires more than transaction data. The agent may need to understand: GraphSentinel therefore uses GraphRAG. The case is embedded and compared with previous cases using both vector similarity and structural relationships such as shared devices and cards. Policy documents are parsed into a document graph containing: DocumentChunk │ ├── PolicyClause ├── Pattern └── Regulation Retrieval combines vector search with graph expansion: User / Case Question │ ▼ Vector Search │ ▼ Relevant Document Chunks │ ▼ Graph Expansion │ ▼ Policy / Pattern / Regulation Context │ ▼ Cited Investigation Context gs similar cases vec gs doc search vec gs policy context This allows the system to attach policy citations to evidence requests, actions, explanations, and SAR decisions. Vector similarity alone is not enough for fraud investigations. Two cases may have similar descriptions but completely different graph structures. Therefore case retrieval combines: Vector similarity + Shared devices + Shared cards + Structural relationships + Previous case outcomes Conceptually: similar cases = retrieve similar cases embedding=current case embedding, graph links= customer id, device ids, card ids, , This allows the risk model to use historical cases without reducing the investigation to a simple nearest-neighbor search. After graph evidence, follow-up investigation, and memory retrieval, the system estimates fraud probability. The risk model is a regularized logistic model. features = bank risk score, amount ratio, device novelty, account age, card velocity, graph fraud proximity, confirmed case links, similar case outcomes, pattern strength, trigger type, fraud probability = model.predict proba features 0, 1 The resulting belief is separated into fraud hypotheses: Legitimate Third-party fraud First-party fraud Evidence can later update those hypotheses. For example, the supplied likelihood tables can make a failed step-up authentication increase the probability of third-party fraud, while a passed authentication shifts belief in the other direction. This is one of the most important parts of the architecture. The policy engine applies configurable thresholds. if probability < CLEAR THRESHOLD: decision = "clear" elif probability ACTION THRESHOLD: decision = "action" else: decision = "uncertain" For uncertain cases, the system evaluates potential evidence requests. But policy is applied before optimization . Suppose we have: Customer validation Step-up authentication Analyst review Device investigation A naive implementation might calculate information gain first. GraphSentinel instead does: Candidate Evidence │ ▼ Policy Filter │ ▼ Allowed Evidence │ ▼ Value of Information │ ▼ Selected Evidence allowed = for request in evidence requests: policy result = policy.check case=case, request=request, if policy result.allowed: allowed.append request best request = max allowed, key=lambda request: expected information gain request - request cost request This matters because a request can be statistically useful while still being impermissible. For example: The optimizer never sees those prohibited requests. Before any additional evidence is requested, the system records the current next-best action. nba before = { "decision": "escalate", "probability": 0.71, "risk level": "high", "fraud class": "third party", "action": "hold transaction", "permission": "approval", "approval route": "fraud analyst", "policy clause": "POL-2.1", } This creates an important property: We know what the system would have done before seeing the additional evidence. Each NBA records information such as: decision probability risk level fraud class selected evidence request excluded requests policy reasons actions permission type approval route policy clause received evidence If the case remains uncertain, the selected evidence request is executed. evidence = { "type": "step up auth", "result": "failed", } The result updates the fraud hypotheses. posterior = bayesian update prior=belief, evidence=evidence, likelihoods=likelihood tables, The important architectural property is that evidence is represented as an explicit transition: Belief Before │ ▼ NBA Before │ ▼ Evidence Request │ ▼ Evidence Response │ ▼ Belief Update │ ▼ NBA After This makes it possible to inspect exactly how new evidence changed the investigation. After updating the belief, the system records a second next-best action. nba after = { "decision": "protect", "probability": posterior.fraud probability, "fraud class": posterior.fraud class, "action": "block transaction", "permission": "approval", "approval route": "fraud analyst", "policy clause": "POL-2.1", } The case now contains a complete decision timeline: Initial Evidence │ ▼ Initial Belief │ ▼ NBA Before Evidence │ ▼ Evidence Request │ ▼ Evidence Response │ ▼ Updated Belief │ ▼ NBA After Evidence This is more informative than simply returning: { "fraud": true } The complete investigation is represented as a state machine. A simplified version looks like: python from langgraph.graph import StateGraph, END workflow = StateGraph InvestigationState workflow.add node "intake", intake workflow.add node "baseline", baseline evidence workflow.add node "followups", followup planning workflow.add node "memory", retrieve memory workflow.add node "assessment", assess risk workflow.add node "decision", policy decision workflow.add node "evidence", request evidence workflow.add node "finalize", finalize case workflow.set entry point "intake" workflow.add edge "intake", "baseline" workflow.add edge "baseline", "followups" workflow.add edge "followups", "memory" workflow.add edge "memory", "assessment" workflow.add edge "assessment", "decision" workflow.add conditional edges "decision", route after decision, { "evidence": "evidence", "finalize": "finalize", }, workflow.add edge "evidence", "assessment" workflow.add edge "finalize", END app = workflow.compile The important property is that an evidence response can send the case back through assessment. The system is therefore: Trigger ↓ Investigate ↓ Assess ↓ Decide ↓ Need evidence? ├── No ───────► Finalize │ └── Yes ↓ Evidence ↓ Reassess ↓ Decide This is the core agentic loop. TigerGraph is the graph system of record for investigation evidence and case memory. The graph contains vertices for: Transaction Customer Device Card Address FraudCase Finding Action DocumentChunk PolicyClause Pattern Regulation Edges represent relationships such as: Customer ──owns──────► Card Customer ──uses──────► Device Customer ──lives at──► Address Transaction ──belongs to──► Customer Case ──has finding──► Finding Case ──has action────► Action Case ──similar to────► Case Document ──references─► PolicyClause This turns the fraud investigation into a connected evidence problem rather than a flat feature table. Every graph read is a named installed query. The contract in graph/contract.py defines query names, parameters, and result parsing. QUERY CONTRACT = { "gs txn detail": "txn id", , "gs customer history": "cust", "as of", "max rows", , "gs address peers": "cust", "window sec", "as of", "min first seen", , "gs similar cases vec": "query vec", "k", , } The local graph store, TigerGraph REST store, and MCP store all implement the same interface. This gives us two major advantages: Representative installed queries include: gs txn detail gs customer history gs device customers gs address peers gs linked cases gs similar cases vec gs doc search vec gs policy context The repository also includes: Weakly Connected Components Louvain Communities Personalized PageRank Personalized PageRank is seeded from confirmed-fraud customers to create a graph-based fraud-proximity feature. The setup also computes: device degrees customer links connected components communities fraud proximity Highly connected hub devices are excluded from useful proximity signals because shared corporate or public devices can otherwise create misleading fraud relationships. The agent-plane MCP adapter exposes four operations: run installed query add nodes add edges get node The architecture becomes: LangGraph Agent │ ▼ TigerGraph MCP │ ├── run installed query ├── get node ├── add nodes └── add edges │ ▼ TigerGraph The MCP server is launched over stdio using the same TigerGraph configuration. The repository also contains an MCP emulator backed by the local graph store. This made it possible to test the complete MCP path without requiring a live TigerGraph deployment. Before investigations run, graph-derived features are precomputed. graph.compute device degrees graph.compute customer links graph.compute wcc graph.compute louvain graph.compute fraud pagerank These values can then be used during investigation instead of repeatedly traversing the entire graph. One of the less obvious challenges was preventing future information from leaking into the investigation. Consider: January 10 │ └── suspicious transaction January 15 │ └── investigation starts January 20 │ └── case confirmed as fraud The January 20 outcome must not become a feature for the January 10 transaction. Therefore graph queries use: as of = case opened at and only retrieve information available before the relevant event. The same principle applies to customer history, account age, devices, cards, and linked cases. Left-censored accounts also need special handling. If the available dataset starts after the account was created, we shouldn't automatically classify that account as "new." These rules belong in the graph access layer and tests, not only in analyst convention. The investigation does not disappear after the final API response. The case is written back to graph memory as a FraudCase . graph.add node "FraudCase", { "id": case.id, "status": case.status, "embedding": case.embedding, }, Findings and actions are then connected: graph.add node "Finding", { "id": finding.id, "type": finding.type, "confidence": finding.confidence, }, graph.add edge "HAS FINDING", case.id, finding.id, The case can be connected to: Customer Focal Transaction Related Transactions Findings Actions Similar Cases This creates a continuous memory loop: Past Investigations │ ▼ Case Memory │ ▼ New Investigation │ ▼ New Findings │ ▼ Updated Memory But agent outcomes and analyst-confirmed outcomes are deliberately kept separate. An agent prediction should not automatically become training ground truth. Only analyst-confirmed outcomes should become authoritative learning data. SAR eligibility is evaluated by policy. The system considers factors such as: Posterior probability Aggregate amount Suspect identification Money-laundering indicators Applicable thresholds When a SAR is required, the agent can draft the narrative: if policy.requires sar case : sar = draft sar case=case, evidence=evidence, citations=policy context, approval queue.submit sar, role="bsa officer", The LLM can help write the narrative, but it does not independently authorize the SAR. The policy engine controls eligibility and the required approval route. The system is agentic in a constrained, auditable sense. It can: The important design choice is that agency is bounded by contracts and policy. The agent can explore and explain. It cannot silently bypass an approval route or turn an uncertain case into an automatic protective action. GraphSentinel also contains a discovery loop for closed investigations. The idea is: Closed Cases │ ▼ Residual Analysis │ ▼ Unexpected Pattern │ ▼ Candidate Rule │ ▼ Policy Review For example, the discovery system might identify an unexplained cluster of newly created accounts sharing the same address. The important part is that discovery does not automatically become policy. The candidate is flagged for review: candidate pattern = { "pattern": pattern, "documented": False, "requires policy review": True, } This keeps pattern discovery separate from authorization. Once the investigation is complete: Automatic actions │ ▼ Mock Action Gateway Approval actions │ ▼ Required Approval Route SAR required │ ▼ BSA Officer Approval All paths │ ▼ Explanation │ ▼ Graph Memory Automatic actions are sent to the mock gateway. Approval actions are queued for the required role. SAR eligibility is evaluated using the policy rules. Finally, the complete case is written back to graph memory. The project can run without TigerGraph for the initial development loop. pip install -e ". dev " graphsentinel synth graphsentinel build graphsentinel run-benchmark graphsentinel eval graphsentinel serve pytest -q The local graph store follows the same query contract as the TigerGraph implementation. For TigerGraph: cp .env.example .env Configure: GS MODE=tigergraph TG variables graphsentinel tg-setup graphsentinel tg-check graphsentinel serve The default agent access path is MCP: GS TG ACCESS=mcp REST access is also supported: GS TG ACCESS=rest Administrative operations such as DDL, bulk loading, and algorithm setup use the REST path. The application also supports the actual HHGOA/IEEE-style dataset through configurable column mappings. The dataset directory is configured with: GS DATA DIR=/path/to/dataset The loader resolves file and column names using: config/dataset mapping.yaml For the provided benchmark case pack: case pack.csv is placed alongside the dataset files. The benchmark can then be run with: GS DATA DIR=/path/to/case-pack \ graphsentinel run-benchmark The generated cases are written as: cases/ ├── HHG-001.json ├── HHG-002.json ├── ... └── HHG-020.json Each investigation record contains the investigation evidence, findings, decisions, actions, graph write-back status, SAR details where applicable, and the next-best action before and after evidence. The project contains two different evaluation modes. The first is the generated benchmark. The second is temporal replay. This distinction is important because the benchmark uses synthetic data where the generator deliberately plants patterns. The benchmark therefore demonstrates that the system behaves correctly against the generated ground truth. It should not be interpreted as a production fraud-detection accuracy estimate. The temporal replay is a more realistic test because the model trains on earlier cases and investigates later cases. The repository reports: Training: Months 1–3 69 closed cases Replay: 37 later cases The replay produced: 21 cases decided directly 20 correct decisions 16 escalations Precision: 1.0 The important limitation is that many escalations came from evidence requests for which no response was recorded in the closed-case data. This means the system sometimes correctly identifies uncertainty but does not have enough historical evidence to resolve it automatically. That is an important difference between: "I don't know" and: "I am confident this is legitimate." A production system should preserve that distinction. A large graph is not automatically useful. Device, card, and address relationships matter when they: The query contract helped keep graph investigation focused on decisions rather than graph traversal for its own sake. Fraud data contains future outcomes, later cases, and accounts that may predate the available dataset. Every query therefore needs an as of boundary. Left-censored accounts must also be handled correctly. Otherwise a seemingly good model can quietly learn from information that would not have been available at decision time. A request can be statistically informative and still be impermissible. Therefore: Evidence candidates ↓ Policy constraints ↓ Allowed candidates ↓ Value-of-information ↓ Selected evidence This ordering prevents an optimizer from selecting a request that creates customer-contact or tipping-off risk. Agent-confirmed and agent-cleared cases are useful memory. But they are not automatically analyst ground truth. Keeping those outcomes separate prevents feedback loops where the model starts training on its own previous decisions. LLM calls can fail because of: Credentials Rate limits Provider changes Malformed JSON Network failures The investigation should still continue. That's why GraphSentinel has deterministic planning and explanation fallbacks. The LLM is an optional reasoning and language layer, not a single point of failure. Execute every GSQL query against a real TigerGraph Savanna or Community Edition deployment and add stronger deployment/version checks. Replace the mock action gateway with authenticated banking, notification, evidence-provider, and e-filing integrations. Replace the development X-Role header with an identity-provider integration and enforce role claims at a trusted proxy boundary. Train and calibrate thresholds on real closed investigations, monitor drift, and add confidence intervals and champion/challenger evaluation. Connect real authentication, customer-validation, and analyst-review systems with asynchronous response handling. Run the complete HHG-001 through HHG-020 case pack and compare decisions with independent review. The current console is a lightweight static analyst UI. A production version would use richer graph interactions, accessibility improvements, and durable event streaming. Add structured traces for: Graph latency Model versions LLM calls Policy decisions Approval turnaround Action outcomes These would be essential for production monitoring. The entire system can ultimately be reduced to this: ┌───────────────────┐ │ TRIGGER │ │ │ │ Risk alert │ │ Customer report │ │ Analyst request │ └─────────┬─────────┘ │ ▼ ┌───────────────────┐ │ LANGGRAPH │ │ AGENT │ └─────────┬─────────┘ │ ┌─────────────┼─────────────┐ │ │ │ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌───────────┐ │TigerGraph│ │ GraphRAG │ │ Case │ │ │ │ │ │ Memory │ │ Evidence │ │ Policies │ │ Similar │ │ Relations│ │ Patterns │ │ Cases │ └────┬─────┘ └────┬─────┘ └─────┬─────┘ │ │ │ └─────────────┼──────────────┘ ▼ ┌───────────────────┐ │ RISK MODEL │ │ │ │ Probability │ │ Fraud class │ └─────────┬─────────┘ │ ▼ ┌───────────────────┐ │ POLICY ENGINE │ │ │ │ Permissions │ │ Approval routes │ │ SAR rules │ └─────────┬─────────┘ │ ▼ ┌───────────────────┐ │ NBA BEFORE │ │ EVIDENCE │ └─────────┬─────────┘ │ uncertain? / \ no yes │ │ │ ▼ │ ┌──────────────┐ │ │ EVIDENCE │ │ │ SELECTION │ │ └──────┬───────┘ │ │ │ ▼ │ ┌──────────────┐ │ │ BELIEF UPDATE│ │ └──────┬───────┘ │ │ │ ▼ │ ┌──────────────┐ │ │ NBA AFTER │ │ │ EVIDENCE │ │ └──────┬───────┘ │ │ └──────┬───────┘ ▼ ┌───────────────────┐ │ ACTION / APPROVAL │ │ SAR / EXPLANATION│ └─────────┬─────────┘ │ ▼ ┌───────────────────┐ │ GRAPH MEMORY │ │ │ │ Case │ │ Findings │ │ Actions │ │ Evidence │ └───────────────────┘ The final system is not an unconstrained chatbot making banking decisions. It is a traceable investigation workflow in which: Graph → provides evidence Risk Model → estimates risk Policy → controls permissions Agent → chooses useful investigation work Human → provides required approvals Action Gateway → executes approved actions Graph Memory → preserves the investigation That separation is the central design principle behind GraphSentinel. The goal is not simply to predict fraud. The goal is to build an investigation system where evidence, reasoning, policy, actions, approvals, and outcomes remain connected and auditable .