LangGraph + Ujex Recall: Long-Term Memory Without Building Your Own
Use LangGraph's BaseStore with Ujex Recall as the backing implementation. Memory becomes .md files in your Cloud Storage bucket; Firestore holds the derived BM25 + vector index. Your agent reads/writes via the LangGraph store API; everything is queryable, greppable, and migratable.
LangGraph's memory documentation distinguishes short-term (thread-scoped, via checkpointer) and long-term (cross-session, via BaseStore). Default backings: SqliteSaver for dev, PostgresSaver for production. Ujex Recall plugs in as the BaseStore.
Why use Ujex Recall instead of Postgres
- Memory is human-readable: each entry is a
.mdfile with frontmatter, not a row in a table - Bucket is the source of truth — Firestore is derived index
- You can
aws s3 ls(or gcs equivalent) and grep your agent's memory at any time - Apache-2.0 SDK, self-hostable, same auth as the rest of your Ujex stack
Setup
pip install langgraph ujex-recall
from langgraph.graph import StateGraph
from ujex_recall import RecallStore
store = RecallStore(api_key=os.environ['UJEX_API_KEY'], agent_id='my-agent')
graph = StateGraph(MyState).compile(store=store)
# In a node:
def remember(state, store):
store.put(('user_facts',), 'preferred_name', 'Akshay')
def recall(state, store):
name = store.get(('user_facts',), 'preferred_name').value
return {'greeting': f'Hi {name}'}
What the bucket looks like
my-agent/
├── _MEMORY.md # auto-generated index
├── user_facts/
│ ├── preferred_name.md
│ ├── timezone.md
│ └── communication_style.md
├── conversations/
│ ├── 2026-01-15/
│ │ └── thread-abc123.md
│ └── ...
└── episodes/
├── ep-001.md
└── ...
Each .md has YAML frontmatter (type, tags, created, updated) followed by content. You can open any of them in a text editor.
Search
# BM25 (default)
results = store.search(('conversations',), query='deployment failure', limit=5)
# Vector (Vertex AI embeddings)
results = store.search(('conversations',), query='deployment failure', limit=5, mode='vector')
# Hybrid
results = store.search(('conversations',), query='deployment failure', limit=5, mode='hybrid')
Migrating from Postgres / Sqlite
Export your existing store to JSON. For each row, write a .md file in the appropriate bucket prefix. The frontmatter holds metadata; the body is content. Re-embed if you want vector search.
Tradeoffs
| SqliteSaver | PostgresStore | Ujex RecallStore | |
|---|---|---|---|
| Local dev fast | ✓ | ✓ (with docker) | ✓ (with emulator) |
| Production scale | ~ small | ✓ | ✓ |
| Human-readable storage | ✗ | ✗ | ✓ (.md files) |
| Cross-session | ✓ | ✓ | ✓ |
| Vector search | External | pgvector | Built-in (Vertex AI) |
| Self-hostable | ✓ | ✓ | ✓ |
What this doesn't replace
The LangGraph checkpointer (short-term thread state) is still SqliteSaver / PostgresSaver. Long-term and short-term are different concerns; only long-term is the store.
FAQ
Does this work in production?
Yes — the SDK is in use at small scale. Cloud Storage and Firestore are battle-tested for the underlying primitives.
Can I use both Postgres (short-term) and Ujex Recall (long-term)?
Yes — they handle different things. Checkpointer for thread state, store for cross-session memory.
What's the latency on a search?
BM25 query: 80–200ms (Firestore composite index). Vector query: 200–500ms (Vertex AI). Hybrid: max of both.