GraphSentinel- Agentic fraud investigation
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.
What We Built
GraphSentinel combines five main ideas:
- A temporal graph investigation layer for transactions, customers, devices, cards, addresses, prior cases, and relationships.
- A risk model trained on closed investigations rather than relying only on a hard-coded fraud score.
- GraphRAG retrieval for policies, fraud patterns, and regulatory references.
- A policy engine that determines what is permitted, what requires approval, and when customer contact is restricted.
- A bounded agent workflow that can select follow-up tools, request evidence, update its belief, and produce an auditable explanation.
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.
Application Support
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.
Architecture
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──โบ (continue)
│
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.
Separation of Powers
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.
The Investigation Workflow
1. Baseline Evidence
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:
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.
2. Follow-up Planning
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
3. Memory and GraphRAG
Fraud investigation requires more than transaction data. The agent may need to understand:
- previous investigations
- internal policies
- known fraud patterns
- regulatory requirements
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
Representative queries include:
-
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.
4. Similar-Case Retrieval
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.
5. Assessment
After graph evidence, follow-up investigation, and memory retrieval, the system estimates fraud probability. The risk model is a regularized logistic model. Conceptually:
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.
6. Policy Decision and Next-Best Action
This is one of the most important parts of the architecture. The policy engine applies configurable thresholds. Conceptually:
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
Conceptually:
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:
- Customer validation can be blocked for customer-reported cases.
- Contact can be restricted when first-party fraud is likely.
- Requests that could create SAR tipping-off risk can be excluded.
The optimizer never sees those prohibited requests.
7. NBA Before Evidence
Before any additional evidence is requested, the system records the current next-best action. For example:
{
"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
8. Evidence Request and Belief Update
If the case remains uncertain, the selected evidence request is executed. For example:
{
"type": "step_up_auth",
"result": "failed"
}
The result updates the fraud hypotheses. Conceptually:
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.
9. NBA After Evidence
After updating the belief, the system records a second next-best action.
{
"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}
10. LangGraph Workflow
The complete investigation is represented as a state machine. A simplified version looks like:
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
Comments
No comments yet. Start the discussion.