A design pattern for persistent TypeDB agents with LangGraph

This example (use-cases/agentic-finance) demonstrates the idiomatic pattern for building an agentic application with TypeDB. At the end of this tutorial we’ll have an agent that is persistent and also highly accurate. Critical business logic are expressed with TypeDB’s powerful query language instead of relying purely on the LLM, minimising agent hallucination.
There are four key elements that we’ve highlighted in this tutorial:
- Building a persistent agent with LangGraph and TypeDB’s persistence plugin
- Defining a guardrail with
typeql-check - Connecting to TypeDB server through the MCP protocol, with TypeDB MCP
- Using TypeDB functions to define logic directly in TypeDB
We use a simple stock market schema in the finance domain. The purpose of the agent we’re building will be to answer English prompts such as “What’s Apple’s market cap?” or “Who underwrote the Acme 2030 bond?” based on live data from the database.
Part 1: Configuring agent persistence
By default, an agent is stateless and the context will be lost if restarted. In order to remedy this issue, we will persist agent context through TypeDB’s LangGraph plugins:
langgraph-checkpoint-typedbwhich will be responsible for persisting conversation historylanggraph-store-typedbwhich will be responsible for persisting long term facts that should span across conversations
driver = connect()
checkpointer = TypeDBSaver(driver, database="langgraph_checkpoints")
checkpointer.ensure_database()
checkpointer.ensure_schema()
store = TypeDBStore(driver, database="langgraph_memory")
store.ensure_database()
store.ensure_schema()
Part 2: Adding typeql-check guardrail
Now that the agent is configured with the necessary persistence plugins, let’s work on adding query guardrail. An LLM does not always produce a correct TypeQL query. Often, it will get the logic right but the syntax wrong. Hence, we will register the typeql-check tool through the MCP protocol along with some instructions to use it.
@tool
def typeql_check(query: str):
"""Validate a TypeQL query's syntax without running it."""
result = subprocess.run([binary, query], capture_output=True, text=True)
return result.returncode
Now that the agent has access to this tool, it can use it to validate generated queries before sending it to the server to be executed.
Part 3: Giving database access through TypeDB MCP
Now let’s configure how the agent can access the data in the database. For this, we’ll use configure the agent using TypeDB MCP server:
client = MultiServerMCPClient(
{"typedb": {"url": "http://localhost:8001/mcp", "transport": "streamable_http"}}
)
mcp_tools = await client.get_tools()
tools = mcp_tools + [typeql_check]
Part 4: Defining logic as TypeDB functions
One decision is left: where the business logic lives. For instance, the market cap of a company isn’t something that we store directly in the database. Instead ot has to be computed using the latest price and number of shares outstanding of a given company. Additionally the latest stock price of a given company itself is not something that is explicitly stored. Instead, it is to be retrieved by finding a stock price attributed that has the most recent timestamp.
We could rely on the LLM to deduce the formula itself and have it perform the appropriate query to retrieve the data relevant for the computation. However it has two risks associated with it:
- It may hallucinate
- It may not have enough domain knowledge if the calculation is not part of general knowledge
The most common approach that people use is to explicitly code the logic at the application-level. This approach does away with hallucination and you can encode any domain-specific logic that the LLM might not know about.
However there’s a third approach: when a computation can strictly be done from data that exist within TypeDB, the computation should be defined as a TypeDB function instead of an application-level logic. This approach has three benefits:
- The code is a lot shorter as we do away deserialising the answer from the database into the application code
- There is no network roundtrip cost as the data and computation are done in the same place. This becomes especially apparent when doing the computation which requires multiple steps
- It is a lot less error prone, as TypeDB ensures the the function declaration is always in sync with the schema. TypeDB validation will warn you if you update the schema without updating the function appropriately.
Here we’ll show an example of defining the computation of latest price and market cap in TypeDB:
fun latest_price($s: security) -> double:
match
pricing (instrument: $s, bar: $b);
$b has price $p, has as_of $t;
let $pv = $p;
sort $t desc;
return first $pv;
fun market_cap($e: equity) -> double:
match
$e has shares_outstanding $n;
let $px in latest_price($e);
let $mc = $px * $n;
return first $mc;
Final part: Defining the agent
We’ve now defined the necessary MCP tools and persistence, and now it’s time to define our agent:
agent = create_react_agent(
model,
tools,
prompt=SYSTEM_PROMPT,
checkpointer=checkpointer,
store=store,
)
The agent will be capable of accepting user prompts and acting on them by executing tool usage and performing database queries appropriately, until the final answer is found. Additionally, as a persistent agent, the context will live on even though the agent experiences crashes or is restarted.
Try it
Ask it about market caps, index constituents, bond syndicates, analyst
ratings, currency pairs. Then quit, restart, and ask a follow-up. The
earlier conversation is still there, checkpointed in TypeDB.
$ docker compose up -d # TypeDB + the TypeDB MCP server
$ poetry env use python3.12 # the LangGraph plugins need 3.11 or 3.12
$ poetry install
$ cp .env.example .env # add your ANTHROPIC_API_KEY
$ poetry run seed # load the schema + sample finance data
$ poetry run agent # start the REPL
Type a message, or 'exit' / Ctrl-D to quit.
you> list all companies in the database
bot> Here are all the companies in the `ticker` database:
| Company | Sector |
|---------|--------|
| Apple Inc | Technology |
| NVIDIA Corp | Technology |
| Tesla Inc | Automotive |
| Acme Corp | Industrials |
There are **4 companies** in total. Note that Acme Corp is not part of the S&P 500 or Nasdaq 100 indices we looked at earlier. Let me know if you'd like more details on any of them.
you> compare the market cap of apple vs nvidia vs tesla
bot> Here's the market cap comparison, ranked from largest to smallest:
| Ticker | Price | Shr. Outstanding | Mkt. Cap |
|------|--------:|---------------:|-----------:|
| AAPL | $213.25 | 15,000,000,000 | $3.20T |
| TSLA | $182.40 | 3,180,000,000 | $580.03B |
| NVDA | $134.80 | 2,460,000,000 | $331.61B |
### Key takeaways
- **Apple** dominates, with a market cap of ~$3.2 trillion — about **5.5×** larger than Tesla and **9.6×** larger than NVIDIA.
- **Tesla** ranks second, largely driven by its high share count despite a mid-range price.
- **NVIDIA** has the lowest market cap of the three, with both the lowest price and fewest shares outstanding.
- Combined, Tesla + NVIDIA (~$911.6B) still amount to less than a third of Apple's market cap.
Let me know if you'd like to add other companies to the comparison or bring in analyst ratings.
Steal the pattern
The finance schema is pretty tiny but should hopefully demonstrate the key components of how to build an end-to-end, persistent agent that looks up its answers directly from TypeDB data store. None of the four key elements making up the agentic system are novel, however care must be taken to implement them right, in order to minimise hallucination and increase reliability. You can take inpsiration from this pattern when building your own TypeDB-powered agentic systems.



