In enterprise liquidity management, cold-start is not a user experience problem; it is a capital efficiency and risk problem. When a new subsidiary joins a cash pool, or when a treasury team onboards a new forecasting module, there is no historical transaction data to train behavioral models. A standard RAG system retrieving generic "liquidity best practices" is useless. The system must provide actionable, entity-specific guidance based on structural attributes (balance sheet composition, industry vertical, banking relationships) until behavioral signals accumulate. This article details an end-to-end implementation of an Adaptive Liquidity Advisor that dynamically switches retrieval strategies based on real-time data maturity assessment, using LangGraph for orchestration and ChromaDB for multi-modal knowledge representation.
The Real-Time Use Case: New Subsidiary Onboarding & Position Analysis
Scenario: A multinational corporation acquires a mid-sized manufacturing firm in Vietnam. The subsidiary’s treasury team logs into the central Liquidity Management System (LMS) for the first time. They ask: "How should we structure our VND cash pooling given our seasonal working capital cycle?"
The Cold-Start Reality:
Zero historical cash flow patterns in the LMS.
No prior recommendation feedback or override history.
ERP integration is partial; only static balance sheet data is available.
Regulatory constraints (VND capital controls) are strict and non-negotiable.
Why Standard RAG Fails: Semantic search on "cash pooling" returns global policies irrelevant to VND regulations or manufacturing seasonality. Without behavioral history, the system cannot distinguish between a high-growth tech subsidiary and a capital-intensive manufacturer.
The Solution: An adaptive agent architecture that classifies the entity’s data maturity state and routes to specialized retrieval strategies: Structural Inference for true cold-start, Hybrid Analog Matching for warm-start, and Behavioral Personalization for mature entities.

Part 1: Data Representation for Liquidity Cold-Start
Cold-start resolution requires representing entities by their structure, not just their behavior. We maintain three distinct ChromaDB collections.
Collection 1: Entity Structural Profiles (entity_structures)
Used when behavioral data is absent. Embeddings capture balance sheet composition and business model semantics.
entity_document = {
"id": "sub_vn_mfg_001",
"page_content": "Manufacturing subsidiary, Ho Chi Minh City. Heavy machinery sector.
Working capital cycle: 90-day inventory, 45-day receivables, 60-day payables.
Capital intensive with quarterly debt service peaks.
Subject to SBV VND transfer restrictions.",
"metadata": {
"type": "entity_structure",
"industry_vertical": "heavy_manufacturing",
"jurisdiction": "VN",
"working_cap_cycle_days": 90,
"debt_service_frequency": "quarterly",
"currency_restrictions": ["VND_capital_control"],
"banking_relationships": ["Vietcombank", "HSBC_VN"],
"data_maturity_tags": ["static_only"]
}
}
Collection 2: Regulatory & Policy Corpus (liquidity_policies)
Regulations are the ultimate cold-start anchor. They apply universally regardless of entity history.
policy_document = {
"id": "reg_vnd_sbv_circular_22",
"page_content": "SBV Circular 22/2023: VND cross-border transfers require underlying
trade documentation. Cash pooling permitted only for entities with
direct equity relationship. Max daily sweep limit: 20% of registered capital.",
"metadata": {
"type": "regulation",
"jurisdiction": "VN",
"regulator": "SBV",
"applicable_currencies": ["VND"],
"effective_date": "2023-11-15",
"constraint_type": "hard_limit"
}
}
Collection 3: Behavioral Patterns (entity_behaviors)
Populated only after sufficient transaction history accumulates. Empty for cold-start entities.
behavior_document = {
"id": "behavior_sg_tech_042",
"page_content": "Consistent Friday cash buildup averaging SGD 2.1M.
Monthly tax payments cause predictable Day-25 drawdowns.
Prefers MMF over term deposits for <30 day horizons.
Override rate on automated sweeps: 8%.",
"metadata": {
"type": "entity_behavior",
"entity_id": "sub_sg_tech_042",
"pattern_confidence": 0.92,
"observation_window_days": 180
}
}
Part 2: Adaptive State Schema
The LangGraph state carries both the maturity assessment and strategy-specific outputs. This prevents downstream agents from assuming uniform data quality.
from typing import Annotated, List, Dict, Optional, Literalfrom typing_extensions import TypedDict
from langgraph.graph.message import add_messages
from langchain_core.documents import Document
MaturityLevel = Literal["true_cold", "warm_start", "mature"]
class LiquidityRecState(TypedDict):
messages: Annotated[List, add_messages]
entity_id: str
# Maturity Assessment
maturity_level: MaturityLevel
available_signals: Dict[str, bool]
inferred_analog_entity: Optional[str] # For warm-start analog matching
# Strategy-Specific Retrieval
retrieved_context: List[Document]
retrieval_strategy: str
# Compliance Gate
regulatory_compliant: bool
compliance_reasoning: str
# Output
recommendation: str
confidence_score: float
data_gap_disclosure: str # Explicit transparency about missing data
agent_trace: List[str]
Part 3: Multi-Agent Implementation
Agent A: Data Maturity Classifier
This node runs first and deterministically assesses what data is actually available. No LLM inference—pure signal inventory.
def maturity_classifier_node(state: LiquidityRecState):
"""Deterministic assessment of available data signals."""
# Fetch from metadata store / ERP integration status API
signals = get_entity_signal_inventory(state["entity_id"])
has_static_profile = signals.get("erp_balance_sheet_connected", False)
has_transaction_history = signals.get("transaction_count_90d", 0) > 30
has_behavioral_patterns = signals.get("pattern_model_trained", False)
has_manual_preferences = signals.get("treasury_preferences_set", False)
if has_behavioral_patterns and has_transaction_history:
level = "mature"
elif has_static_profile or has_manual_preferences:
level = "warm_start"
else:
level = "true_cold"
return {
"maturity_level": level,
"available_signals": signals,
"agent_trace": [f"classifier: maturity={level}, signals={signals}"]
}
Agent B: Adaptive Retrieval Router
Routes to fundamentally different retrieval strategies based on maturity. This is the core cold-start handling mechanism.
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
# Initialize stores (assume embeddings configured elsewhere)
structure_store = Chroma(collection_name="entity_structures", embedding_function=OpenAIEmbeddings())
policy_store = Chroma(collection_name="liquidity_policies", embedding_function=OpenAIEmbeddings())
behavior_store = Chroma(collection_name="entity_behaviors", embedding_function=OpenAIEmbeddings())
def adaptive_retriever_node(state: LiquidityRecState):
query = state["messages"][-1].content
level = state["maturity_level"]
if level == "true_cold":
# STRATEGY: Regulatory-first + jurisdiction defaults
# No entity-specific retrieval possible; anchor entirely on hard constraints
docs = policy_store.similarity_search(
query, k=5,
filter={"jurisdiction": {"$in": get_entity_jurisdictions(state["entity_id"])}}
)
strategy = "regulatory_defaults_only"
gap_disclosure = "No transaction history or profile data available. Recommendations based solely on regulatory framework."
elif level == "warm_start":
# STRATEGY: Structural analog matching + policy overlay
# Find structurally similar entities WITH behavioral history
analog_docs = structure_store.similarity_search(query, k=3)
analog_entity_id = analog_docs[0].metadata.get("entity_id") if analog_docs else None
# Retrieve policies for this entity's jurisdiction
policy_docs = policy_store.similarity_search(
query, k=3,
filter={"jurisdiction": {"$in": get_entity_jurisdictions(state["entity_id"])}}
)
docs = analog_docs + policy_docs
strategy = "structural_analog_matching"
gap_disclosure = f"Limited history. Insights derived from structurally similar entities in {analog_docs[0].metadata.get('industry_vertical', 'similar')} sector."
else: # mature
# STRATEGY: Full behavioral personalization + policy validation
behavior_docs = behavior_store.similarity_search(
query, k=5,
filter={"entity_id": state["entity_id"]}
)
policy_docs = policy_store.similarity_search(query, k=2)
docs = behavior_docs + policy_docs
strategy = "full_behavioral_personalization"
gap_disclosure = ""
return {
"retrieved_context": docs,
"retrieval_strategy": strategy,
"inferred_analog_entity": state.get("inferred_analog_entity"),
"data_gap_disclosure": gap_disclosure,
"agent_trace": state["agent_trace"] + [f"retriever: strategy={strategy}, docs={len(docs)}"]
}
Agent C: Regulatory Compliance Validator
Hard gate that applies regardless of maturity level. Cold-start recommendations are more vulnerable to compliance failures because they lack behavioral guardrails.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
compliance_prompt = ChatPromptTemplate.from_messages([
("system", """You are a Treasury Regulatory Compliance Auditor.
Validate this liquidity recommendation against retrieved regulations.
ENTITY JURISDICTION: {jurisdiction}
MATURITY LEVEL: {maturity} (cold-start requires EXTRA scrutiny)
Check for:
1. Currency transfer restrictions
2. Cash pooling eligibility requirements
3. Sweep limit violations
4. Documentation requirements
Return JSON: {{"compliant": bool, "reasoning": string}}"""),
("human", "Recommendation Context: {context}\nQuery: {query}")
])
def compliance_validator_node(state: LiquidityRecState):
chain = compliance_prompt | llm.with_structured_output(dict)
result = chain.invoke({
"jurisdiction": get_entity_jurisdictions(state["entity_id"]),
"maturity": state["maturity_level"],
"context": "\n---\n".join(d.page_content for d in state["retrieved_context"]),
"query": state["messages"][-1].content
})
return {
"regulatory_compliant": result["compliant"],
"compliance_reasoning": result["reasoning"],
"agent_trace": state["agent_trace"] + [f"compliance: passed={result['compliant']}"]
}
Agent D: Confidence-Aware Synthesizer
Explicitly communicates data limitations. Cold-start responses must never present inferred insights as established facts.
synth_prompt = ChatPromptTemplate.from_messages([
("system", """You are an Enterprise Liquidity Management Advisor.
MATURITY: {maturity}
STRATEGY: {strategy}
DATA GAP DISCLOSURE: {gap_disclosure}
COMPLIANCE STATUS: {compliant} ({compliance_reasoning})
RESPONSE RULES BY MATURITY:
- true_cold: Provide ONLY regulatory-compliant baseline guidance.
Never suggest specific amounts or timings. Always end with data onboarding next steps.
- warm_start: Frame insights as "based on similar entities."
Clearly distinguish analog-derived vs. regulation-derived guidance.
- mature: Standard personalized recommendations with normal confidence.
ALWAYS include the data gap disclosure verbatim if non-empty.
Retrieved Context: {context}"""),
("human", "{query}")
])
def synthesizer_node(state: LiquidityRecState):
if not state["regulatory_compliant"]:
return {
"recommendation": "Unable to provide recommendation: regulatory compliance check failed. "
f"Reason: {state['compliance_reasoning']}. "
"Please consult your regional treasury compliance officer.",
"confidence_score": 0.0
}
chain = synth_prompt | llm
response = chain.invoke({
"maturity": state["maturity_level"],
"strategy": state["retrieval_strategy"],
"gap_disclosure": state["data_gap_disclosure"],
"compliant": state["regulatory_compliant"],
"compliance_reasoning": state["compliance_reasoning"],
"context": "\n---\n".join(d.page_content for d in state["retrieved_context"]),
"query": state["messages"][-1].content
})
# Confidence reflects data maturity, not just retrieval quality
base_confidence = {"true_cold": 0.35, "warm_start": 0.60, "mature": 0.90}
return {
"recommendation": response.content,
"confidence_score": base_confidence[state["maturity_level"]],
"messages": [("assistant", response.content)],
"agent_trace": state["agent_trace"] + ["synthesizer: response generated"]
}
Part 4: Compiling the Adaptive Graph
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.postgres import PostgresSaver
workflow = StateGraph(LiquidityRecState)
workflow.add_node("classify_maturity", maturity_classifier_node)
workflow.add_node("adaptive_retrieve", adaptive_retriever_node)
workflow.add_node("validate_compliance", compliance_validator_node)
workflow.add_node("synthesize", synthesizer_node)
workflow.add_edge(START, "classify_maturity")
workflow.add_edge("classify_maturity", "adaptive_retrieve")
workflow.add_edge("adaptive_retrieve", "validate_compliance")
def compliance_router(state: LiquidityRecState):
# Always synthesize—even failures need compliant messaging
return "synthesize"
workflow.add_conditional_edges("validate_compliance", compliance_router)
workflow.add_edge("synthesize", END)
checkpointer = PostgresSaver.from_conn_string("postgresql://...")
app = workflow.compile(checkpointer=checkpointer)
Part 5: Cold-Start Quality Measurement
Standard engagement metrics are meaningless for cold-start liquidity recommendations. Use these instead:
| Maturity Stage | Primary Metric | Target | Rationale |
|---|---|---|---|
| True Cold | Data Onboarding Completion Rate | >70% within 14 days | System success = accelerating transition out of cold-start |
| Warm Start | Analog Recommendation Acceptance Rate | >50% | Validates structural similarity matching accuracy |
| Mature | Forecast Accuracy Improvement vs. Baseline | >15% MAPE reduction | Behavioral personalization must demonstrably improve outcomes |
| All Stages | Regulatory Violation Rate | 0% | Non-negotiable safety floor |
Implementation: Tag every recommendation event with maturity_level and retrieval_strategy. Run separate funnel analyses per stage. Track the velocity of maturity transitions—your cold-start system succeeds when entities stop being cold-start.
Production Hardening
Analog Validation Guardrails: For warm-start, never use analogs from different regulatory jurisdictions. Add hard filters:
filter={"jurisdiction": entity_jurisdiction}on analog searches. Cross-jurisdiction analogs in liquidity management create compliance risk.Maturity Transition Automation: When transaction count crosses threshold or behavioral model training completes, trigger async job to populate
entity_behaviorscollection. Don’t wait for next query. Log transition events for audit.Confidence Score Surfacing: Expose
confidence_scoreanddata_gap_disclosurein the UI. Treasury users need to calibrate trust. Low-confidence responses should render with distinct visual treatment and explicit next-step actions ("Connect ERP to improve accuracy").Regulatory Corpus Versioning: Policies change. Include
effective_dateandsuperseded_bymetadata. Auto-expire stale regulations. Cold-start entities are especially vulnerable to outdated policy retrieval since they lack behavioral correction signals.Feedback Capture for Cold-Start: Even when behavioral data is absent, capture explicit feedback (thumbs up/down, "not applicable to my entity"). This accelerates warm-start calibration and flags bad analog matches before they compound.
Conclusion
Cold-start in fintech liquidity management is solved not by better embeddings but by honest data representation and adaptive orchestration. By maintaining separate structural, regulatory, and behavioral collections, classifying maturity deterministically before retrieval, routing to strategy-appropriate search methods, and explicitly disclosing data gaps in synthesized responses, you transform cold-start from a failure mode into a guided onboarding experience that respects both capital risk and regulatory reality. The architectural principle: your system must know what it doesn’t know, communicate that limitation transparently, and have a deterministic path toward knowing more. In liquidity management, that honesty is worth more than any confident-sounding hallucination.

Join the conversation! Your thoughts help the community grow.