FishMem

How to add persistent memory to a LangGraph agent with FishMem

How to add persistent memory to a LangGraph agent with FishMem
August 23, 2026Guides8 min read

Add scoped recall before a LangGraph agent acts and durable writes after a verified outcome. This guide uses the universal FishMem SDK and keeps checkpoints separate from long-term memory.

LangGraph can checkpoint a workflow and resume it later. That does not automatically give the agent durable knowledge about a user, a project, or an outcome that should influence a different run.

This tutorial adds FishMem at two explicit graph boundaries: recall before the node that needs long-term context, and write only after the graph has produced a verified conclusion worth keeping.

What you will build

The example is a support graph with three stages:

  1. Recall prior customer preferences, constraints, and verified outcomes.
  2. Act with those memories included as a labeled context block.
  3. Remember the final verified resolution under the same trusted user and agent scopes.

FishMem does not publish a LangGraph-specific adapter. The integration uses the universal TypeScript SDK at normal graph boundaries, which keeps the memory contract visible and makes the same pattern usable in other workflow frameworks.

Prerequisites

  • Node.js or another TypeScript runtime supported by the FishMem SDK.
  • A LangGraph application or a small graph you can extend.
  • A server-side FishMem Cloud API key, or the base URL and key for a self-hosted FishMem service.
  • An authenticated user identity that comes from your application, not from model output.

Install the graph runtime and the universal SDK:

pnpm add @langchain/langgraph @fishmem/sdk

Step 1: Create the FishMem client on the server

Keep the project API key in the server, Worker, or edge runtime. Never include it in browser code or graph state that can be serialized to an untrusted client.

import { FishMem } from "@fishmem/sdk";

const fishmem = new FishMem({
  apiKey: process.env.FISHMEM_API_KEY!,
});

For a self-hosted deployment, pass the service URL explicitly:

const fishmem = new FishMem({
  apiKey: process.env.FISHMEM_API_KEY!,
  baseUrl: "https://memory.example.com",
});

Step 2: Define state for recalled memory and verified outcomes

The graph needs a place for recalled memories, but it should not let the model choose the authenticated user. Inject that identity when the server invokes the graph.

import { Annotation } from "@langchain/langgraph";

export const SupportState = Annotation.Root({
  latestUserMessage: Annotation<string>,
  authenticatedUserId: Annotation<string>,
  caseId: Annotation<string>,
  recalledMemories: Annotation<string[]>,
  draftReply: Annotation<string>,
  verifiedOutcome: Annotation<string | undefined>,
  memoryEventId: Annotation<string | undefined>,
});

export type SupportStateValue = typeof SupportState.State;

In a larger application, the authenticated identity may live outside persisted graph state entirely. The invariant is the same: derive it from trusted server authentication and do not accept an arbitrary project or user scope from the model.

Step 3: Add a recall node before the agent acts

Search with the current user message and the same structural scopes used for writes. Use a stable agent_id so memories for one workflow do not silently mix with another.

async function recallNode(state: SupportStateValue) {
  const { results } = await fishmem.memories.search({
    query: state.latestUserMessage,
    user_id: state.authenticatedUserId,
    agent_id: "support-graph",
    top_k: 6,
  });

  return {
    recalledMemories: results.map((item) => item.memory),
  };
}

Recall is bounded on purpose. The agent does not need every memory associated with the user. It needs a small set that can affect the current decision.

Step 4: Build a labeled memory block

Keep retrieved memory separate from system instructions and source quotations. The model should understand that the block contains recalled user and workflow context, not new commands.

function formatMemoryContext(memories: string[]) {
  if (memories.length === 0) {
    return "No relevant durable memories were recalled.";
  }

  return [
    "Relevant durable memories:",
    ...memories.map((memory) => `- ${memory}`),
  ].join("\n");
}

Your existing model node can insert this block into its instructions or input:

async function agentNode(state: SupportStateValue) {
  const memoryContext = formatMemoryContext(state.recalledMemories);

  const result = await runSupportModel({
    userMessage: state.latestUserMessage,
    memoryContext,
  });

  return {
    draftReply: result.reply,
    verifiedOutcome: result.verifiedOutcome,
  };
}

The example leaves runSupportModel as the model-specific part of your application. FishMem only owns durable memory; it does not require one LLM provider or replace the LangGraph execution model.

Step 5: Write only after a verified outcome

A common mistake is writing every intermediate thought or tool result to long-term memory. In a support workflow, the durable record is usually the verified resolution, a corrected account fact, or a troubleshooting step that should influence the next case.

Queue the write with an idempotency key derived from the business event:

async function rememberNode(state: SupportStateValue) {
  if (!state.verifiedOutcome) {
    return {};
  }

  const receipt = await fishmem.memories.addAsync(
    {
      content: state.verifiedOutcome,
      user_id: state.authenticatedUserId,
      agent_id: "support-graph",
      metadata: {
        source: "verified-support-outcome",
        case_id: state.caseId,
      },
    },
    { idempotencyKey: `case:${state.caseId}:outcome` },
  );

  return { memoryEventId: receipt.event_id };
}

The default inferred path returns a durable event receipt. The graph can store the event ID, observe it later, or wait when the product needs final records before continuing:

const completed = await fishmem.events.wait(receipt.event_id);
console.log(completed.results);

If verifiedOutcome is already the exact canonical record and must be stored verbatim, use the synchronous infer: false path instead of asking another model to extract it.

Step 6: Wire the nodes into the graph

import { END, START, StateGraph } from "@langchain/langgraph";

export const supportGraph = new StateGraph(SupportState)
  .addNode("recall", recallNode)
  .addNode("agent", agentNode)
  .addNode("remember", rememberNode)
  .addEdge(START, "recall")
  .addEdge("recall", "agent")
  .addEdge("agent", "remember")
  .addEdge("remember", END)
  .compile();

The placement is deliberate:

  • Recall happens before the node that needs durable context.
  • The model and tools run with a bounded, labeled memory block.
  • The write happens only after the graph identifies a durable outcome.

Step 7: Invoke the graph with trusted scope

const result = await supportGraph.invoke({
  latestUserMessage: "The export still fails after the browser restart.",
  authenticatedUserId: session.user.id,
  caseId: "case_8421",
  recalledMemories: [],
  draftReply: "",
  verifiedOutcome: undefined,
  memoryEventId: undefined,
});

On a later run, invoke the graph with the same authenticated user ID and agent_id. The recall node can now surface earlier outcomes without replaying the complete ticket history.

Checkpoint state and long-term memory are complementary

Use LangGraph checkpoints to resume execution state: which node ran, what tool returned, or where a human approval paused the workflow. Use FishMem for durable conclusions that should cross checkpoints and runs.

  • A tool call result needed by the next node belongs in graph state.
  • A user preference that should shape future cases belongs in durable memory.
  • A policy manual belongs in source RAG, not a user memory record.
  • A verified resolution may belong in memory after the case closes.

This separation prevents a graph checkpoint from becoming an accidental customer profile and prevents long-term memory from filling with temporary orchestration details.

Production checklist

  • Derive scope on the server. Do not accept project or user identity from model output.
  • Keep writes idempotent. Use a stable key tied to the case, order, run, or verified event.
  • Bound recall. Start with a small top_k and measure whether each recalled record changes the answer.
  • Handle empty recall. The agent should still work when no memory is relevant or the memory service returns no candidates.
  • Observe asynchronous completion. Persist the event ID when the write can finish after the graph response.
  • Keep sources separate. Retrieve manuals and policies through document search so the answer can retain citations.
  • Test corrections. Your acceptance set should include changed preferences, superseded facts, deletion, and retries.

Cloud or self-hosted?

The graph code stays almost identical. FishMem Cloud manages the API, project keys, workspace isolation, metering, durable workers, and dashboard. A self-hosted deployment gives your team operational control and responsibility. Choose based on the authority and recovery boundary your application needs.

FishMem does not claim a native LangGraph adapter. The integration is an explicit use of the universal SDK, which makes every scope, search, and durable write visible in application code.

Frequently asked questions

Should recall be a LangGraph tool?

It can be, but a fixed recall node is often safer for required context because the application controls when it runs and which trusted scopes it uses. Tool-based recall is useful when the model should decide whether another query is needed.

Should the graph wait for every memory write?

No. Use the asynchronous receipt when the response does not depend on the final extracted records. Wait only when the current workflow genuinely requires the committed result.

Can several agents share memory?

Yes, but define the ownership deliberately. Shared user knowledge can use the same user_id; agent-specific operating knowledge should retain distinct agent_id values. The authenticated project remains the outer isolation boundary.

What if a recalled memory is wrong?

Keep the memory ID and retrieval evidence available to application logs, then use explicit update, feedback, invalidation, or deletion flows. Do not silently rewrite a record based only on one model response.

Next steps

Read the canonical FishMem LangGraph integration guide, the TypeScript SDK quickstart, and the support-agent cookbook. For the architectural boundary behind this graph, see What is agent memory?

You can inspect the open-source engine or create a free FishMem Cloud workspace and test the graph with two separate runs.

Read next