Docs
View as markdown

Use relate with LangChain

Give a LangChain agent a recall tool that reads an agent's memory from a relate store on a read-only connection, checked before any model call.

pythonagents

Before you start: Embed relate in a Python app

This guide shows you how to give a LangChain agent a tool that recalls what an agent's memory holds, from a relate store A relate database: a directory, a single .rl file, or :memory:, opened with --database. Read more in the glossary, without letting the agent change it. Your app writes the memory; the agent gets one recall tool that opens the store read-only An open mode (--read-only) that never writes, checkpoints or migrates the store; writes fail, and a directory store can be read beside its writer. Read more in the glossary on every call and follows the graph from a name to the episodes that mention it and the people who work on it. The tool runs without a model, so you can check what the agent gets before it makes a call.

Prerequisites

  • Python 3.10 or later, with the relate client A language package that runs the relate engine inside a program's process: the Python, Node.js, Rust, Java, Go and Swift clients. Read more in the glossary and LangChain: pip install relate-db langchain. The package is relate-db; the import is import relate. The LangChain code on this page was checked against langchain 1.4.9 and langchain-core 1.7.6, as of 2026-11-08.
  • For Give the tool to an agent only: a chat model's integration package and credential, such as pip install "langchain[anthropic]" with ANTHROPIC_API_KEY in the environment.
  • relate on your PATH for Verify (Install relate).

How this differs from the LangGraph guide

Use relate with LangGraph builds a StateGraph by hand, whose steps write and recall memory. This page hands a read-only tool to LangChain's create_agent, which builds the agent loop itself:

This pageThe LangGraph guide
Who writes memoryYour app, on its own writable connectionThe graph's remember tool
What the agent can doRead, through recallWrite and read, through remember and recall
The connectionA new read-only one per callOne writable connection on a dedicated thread
The agent loopcreate_agentA StateGraph with two nodes

Both store memory the same way: each turn is an Episode node An entity in the graph: it carries zero or more labels and a map of properties, and relationships connect it to other nodes. Read more in the glossary IN_CONVERSATION a Conversation, and an episode MENTIONS the Person and Project nodes of the team graph the Get started pages use. Model agent memory explains the model this one reduces.

Write the tool

The program opens ./memory, a directory store A store kept as a folder of snapshot files, a WAL folder and lock files; the layout for any --database path that is not :memory: and does not end in .rl. Read more in the glossary, for writing in a setup block that stands in for the app: it writes the team graph and two episodes from two stand-ups. The recall tool opens its own read-only connection on each call. The last lines print the tool as the model sees it and call it twice, the way the agent does, with a name that exists and one that doesn't:

recall_tool.py
import relate
from langchain.tools import ToolException, tool

TEAM = """
CREATE (ada:Person {name: 'Ada', role: 'engineer'}),
       (grace:Person {name: 'Grace', role: 'engineer'}),
       (noor:Person {name: 'Noor', role: 'maintainer'}),
       (relate:Project {name: 'relate', language: 'Rust'}),
       (tck:Project {name: 'opencypher-tck', language: 'Gherkin'}),
       (ada)-[:WORKS_ON {since: 2024}]->(relate),
       (grace)-[:WORKS_ON {since: 2023}]->(relate),
       (grace)-[:WORKS_ON {since: 2022}]->(tck),
       (noor)-[:MAINTAINS]->(relate),
       (ada)-[:KNOWS]->(grace)
"""

REMEMBER = """
MERGE (c:Conversation {id: $conversation})
CREATE (e:Episode {seq: $seq, text: $text})-[:IN_CONVERSATION]->(c)
WITH e
UNWIND $mentions AS name
MATCH (x {name: name})
MERGE (e)-[:MENTIONS]->(x)
"""

STORE = "./memory"

# Setup: the app that owns ./memory writes the team graph and two episodes, on a writable connection.
with relate.connect(STORE) as owner:
    if owner.execute("MATCH (p:Person) RETURN count(p) AS n").single()["n"] == 0:
        owner.execute(TEAM)
        owner.execute(REMEMBER, {"conversation": "standup-monday", "seq": 1, "text": "Ada fixed the CSV import bug in relate.", "mentions": ["Ada", "relate"]})
        owner.execute(REMEMBER, {"conversation": "standup-tuesday", "seq": 2, "text": "Grace will review the relate release notes.", "mentions": ["Grace", "relate"]})


@tool(parse_docstring=True)
def recall(name: str) -> list[str]:
    """Return what memory holds about a person or project: the episodes that mention it, oldest first, then who works on it.

    Args:
        name: The exact name of a person or project, such as "Ada" or "relate".
    """
    # Read-only: the engine refuses every write on this connection.
    with relate.connect(STORE, read_only=True) as conn:
        if conn.execute("MATCH (x {name: $name}) RETURN count(x) AS n", {"name": name}).single()["n"] == 0:
            raise ToolException(f"Nothing in memory is named {name!r}.")
        episodes = conn.execute(
            """
            MATCH (x {name: $name})<-[:MENTIONS]-(e:Episode)-[:IN_CONVERSATION]->(c:Conversation)
            RETURN c.id AS conversation, e.text AS text
            ORDER BY e.seq
            """,
            {"name": name},
        ).fetchall()
        people = conn.execute(
            """
            MATCH (x {name: $name})<-[w:WORKS_ON]-(p:Person)
            RETURN p.name AS person, w.since AS since
            ORDER BY since
            """,
            {"name": name},
        ).fetchall()
    return [f"[{r['conversation']}] {r['text']}" for r in episodes] + [
        f"{r['person']} has worked on {name} since {r['since']}" for r in people
    ]


# A ToolException goes back to the model as the tool's answer instead of ending the run.
recall.handle_tool_error = True

# Check: what the model sees, then two calls the way an agent makes them.
print(recall.name, recall.args)
for line in recall.invoke({"name": "relate"}):
    print(line)
print(recall.invoke({"name": "relate-engine"}))
recall {'name': {'description': 'The exact name of a person or project, such as "Ada" or "relate".', 'title': 'Name', 'type': 'string'}}
[standup-monday] Ada fixed the CSV import bug in relate.
[standup-tuesday] Grace will review the relate release notes.
Grace has worked on relate since 2023
Ada has worked on relate since 2024
Nothing in memory is named 'relate-engine'.

The first call returns both episodes about relate, oldest first, and then two lines that no episode stated: they come from the team graph's WORKS_ON relationships. The second call returns the ToolException message as the tool's answer. The parts that matter:

  • A connection per call. create_agent runs a synchronous tool on a worker thread, and a relate connection belongs to the thread that opened it. Opening inside the tool keeps the open and the statements on one thread, and each call sees every episode the app committed before it.
  • parse_docstring=True takes the name argument's description from the docstring's Args: section, so the model gets it in the tool's schema; the first line printed shows it.
  • handle_tool_error = True turns a ToolException into the tool's answer, so a misspelled name gets a sentence the model can act on instead of ending the run.
  • Every value is a $parameter. The model chooses only name; the Cypher is fixed, and the read-only connection refuses a write even if a statement tried one.

Give the tool to an agent

create_agent takes a model name and the tools. Replace the check lines at the end of recall_tool.py with this; it needs the model's credential:

from langchain.agents import create_agent

agent = create_agent(
    "anthropic:claude-opus-5-5",
    tools=[recall],
    system_prompt="Answer from memory. Call recall with the exact name of a person or project.",
)
result = agent.invoke({"messages": [{"role": "user", "content": "What happened on relate this week?"}]})
print(result["messages"][-1].content)

The model name is <provider>:<model>, and the provider's package must be installed. The model can call recall, read the lines it returns, and answer from them.

Verify

The tool's output already shows what it read. To see the links it followed, ask the store from the CLI, read-only, in the directory the program ran in:

relate --database ./memory --read-only --mode table "MATCH (e:Episode)-[:MENTIONS]->(x) RETURN e.seq AS seq, x.name AS mentions ORDER BY seq, mentions"
+-----+----------+
| seq | mentions |
+-----+----------+
| 1   | "Ada"    |
| 1   | "relate" |
| 2   | "Grace"  |
| 2   | "relate" |
+-----+----------+

Each episode links to the person and the project it names, which is the path recall follows from relate back to both episodes.

If it goes wrong

MessageCauseRemedy
Relate connection used from a thread other than the one that opened it: connections are thread-affine (DB-API threadsafety = 1). Create the connection on the thread that uses it. (relate.ProgrammingError)The tool used a connection opened outside it, such as one at module level, from the worker thread create_agent runs it onOpen the connection inside the tool, as recall does.
storage error: read-only open requires an existing database at <path> (relate.OperationalError)STORE names a directory that holds no store; a read-only open never creates oneCreate the store with a writable open first, as the setup block does, or fix the path.
storage locked: <path> (relate.OperationalError)The store is a single-file .rl store that the app holds for writing, and a read-only open refuses itKeep memory in a directory store, as ./memory is, so the tool reads beside the app.
Initializing ChatAnthropic requires the langchain-anthropic package. Please install it with `pip install langchain-anthropic` (ImportError)create_agent got an anthropic: model name without the provider's packageInstall langchain[anthropic], or the package for the provider you name.

The errors reference lists the Python exception class for each engine error.