Artificial Intelligence Blogs Posts
cancel
Showing results for 
Search instead for 
Did you mean: 

For the past year, I’ve been experimenting and building AI agents with LangChain/LangGraph inside the CAP Node.js runtime on SAP BTP. This has been less about “toy agents” and more about understanding what it actually takes to run agents reliably in SAP landscapes, next to real business systems and users.

When you look at AI agents as a software architecture (not just “LLM + tools”) and are planning to take it to production, you face the critical requirements: identity, authn/authz, multitenancy, persistence, event‑driven flows, scalability, observability, CI/CD, and so on. CAP already solves most of this for applications on BTP, which is why I’m increasingly convinced that “LangChain/LangGraph + CAP Node.js” is one of the best combinations for taking agents to production in SAP environments. I’ll share a more detailed take on that combo in a separate post.

In this post, let’s narrow the scope and focus on one foundational capability: durability — the ability for an agent to maintain state, survive failures, and resume multi‑step, long‑running tasks without losing its context. We’ll see how LangGraph’s checkpointing model works, and how a CAP‑native LangGraph/LangChain Checkpointer (via @mi8y/cds-langgraph-persistence) lets you bring durable agents to SAP BTP with minimal friction.


Thinking of agents as state machines

A useful mental model is to treat an AI agent as a "state machine." At any point in time, the agent has a “state”: the current conversation history, internal variables, partial results, tool outputs, and whatever else it needs to decide the next step in its workflow.

AI Agent as State Graph

As the agent works towards a goal, this "state" changes: new messages are added, tools are called, intermediate decisions are made. If you could take a snapshot of that state on every transition and store it in durable persistence, you would be able to:

  • Recover from crashes or infra failures by reloading the latest snapshot.
  • Resume long‑running workflows after delays (e.g. waiting for human approval).
  • Inspect what the agent did, step by step.
  • “Time‑travel” back to a previous checkpoint if needed. (useful for forking or replaying in case of an UI trying to catch up with a long-running agent)

This is exactly what LangGraph’s checkpointers are about: they provide a persistence layer that saves a snapshot of the graph’s state on each step and make it retrievable later, keyed by a thread or workflow id.


What are LangGraph Checkpoints?

LangGraph offers a checkpointing interface that you plug into your agent graph when you compile it. Each time the graph advances, the checkpointer records a snapshot of the state. These snapshots are stored somewhere (in‑memory or in a database), and referenced by a thread_id or similar key so you can resume or inspect a specific conversation. Please note that LangGraph, LangChain and DeepAgents all share the same checkpointing interface, so you can swap implementations without changing your agent logic.

Checkpointing in LangGraph

This unlocks

  • Conversational memory: the agent remembers previous turns without you manually managing history.
  • Fault tolerance: if the app restarts, you can reload the last checkpoint and continue.
  • Time travel and introspection: you can inspect or replay previous states.
  • Human‑in‑the‑loop: you can pause at a checkpoint, wait for human input, and resume from there.

The simplest way to get started is the in‑memory checkpointer that LangGraph ships with:

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

const checkpointer = new MemorySaver(); // in-memory; good for dev, loses state on restart

const agent = createAgent({
  checkpointer, // just pass the instance
  // other settings: model, tools, etc.
});

This is perfect for local experimentation and debugging, but it only keeps checkpoints in memory. Once your CAP app restarts or scales, those checkpoints are gone. For SAP BTP and CAP, you need something that speaks to your actual persistence layer and fits into your multi‑tenant, production setup.


Why a CAP/CDS‑native checkpointer?

Out of the box, LangGraph/LangChain provide checkpointer implementations for stores like SQLite (good for local dev), Postgres, Redis and other generic backends for production. In a typical non‑SAP BTP environment, that’s often enough.

On SAP BTP with CAP and SAP HANA Cloud, things are slightly more nuanced:

  • You might be using HDI containers per tenant, with separate schemas.
  • You might just have a single HANA database
  • You need agents to run locally for dev and in CI pipelines for agent evals without heavyweight setup.
  • You don’t want to bolt on an entirely different persistence stack just for agent state, bypassing CAP’s data model and lifecycle.

merbinjanselm_0-1784130661052.png

To solve this, I'm announcing an NPM package - @mi8y/cds-langgraph-persistence. This combines two roles:

  • A CDS plugin that wires the required persistence entities into your CAP data model.
  • A LangGraph/LangChain checkpointer adapter that uses those entities to store and retrieve checkpoints.

This means:

  • You stay within CAP’s multitenancy and lifecycle model.
  • You don’t need to manually create or deploy tables for checkpoints.
  • The same code works locally, in CI, and on SAP BTP, as long as CAP can talk to the configured database.

The code is opensource and MIT licensed. The package is designed to work with CAP v9 and the latest v10, and acts as a drop‑in replacement for any checkpointer you might already be using.

Here’s what swapping looks like at the agent level:

import { CdsCheckpointSaver } from "@mi8y/cds-langgraph-persistence";

- const checkpointer = new MemorySaver(); // any non-CAP checkpointer
+ const checkpointer = new CdsCheckpointSaver({
+   name: "my-agent",
+ });

const agent = createAgent({
  checkpointer,
  // ...
});

Internally, the CDS plugin injects the necessary entities into your model, while CAP takes care of tenant routing, schema management, and lifecycle. You get durability and multitenancy “for free”, without custom plumbing.


Multi‑tenancy and lifecycle, the CAP way

Because the plugin adds entities directly into your CDS model, CAP treats checkpoint data like any other CAP‑managed entity:

  • Multitenancy: if your CAP app is configured for multitenancy, CAP routes checkpoint reads/writes to the correct tenant container based on the request context. You don’t have to manually add tenant IDs to agent state or build your own sharding logic.
  • Lifecycle: you don’t need to deploy checkpoint tables separately. CAP’s usual deployment flow handles entity lifecycle, whether you’re using SQLite, HANA, Postgres or any custom DB adapter during development and production.

Getting started in a CAP project

Let’s walk through a minimal example of wiring a LangGraph agent with CAP and the CDS checkpointer.

1. Install the package

In your CAP Node.js project:

npm install @mi8y/cds-langgraph-persistence

2. Add CDS entities

Run the following command to add the necessary CDS entities

cds add langgraph-persistence

3. Define a simple agent service

Expose a REST action that will drive the agent, passing in a threadId and the latest user message:

@protocol: 'rest'
service AgentService {
  action invoke(threadId: String, content: String) returns String;
}

4. Add a custom CDS handler with the checkpointer

In your service implementation, initialize the checkpointer and agent once, then use them inside the action handler:

import cds from "@sap/cds";
import { CdsCheckpointSaver } from "@mi8y/cds-langgraph-persistence";
import { createAgent } from "./agent"; // your LangGraph agent factory

export class AgentService extends cds.ApplicationService {
  init() {
    // Initialize a CAP-aware checkpointer for an agent named "my-agent"
    const checkpointSaver = new CdsCheckpointSaver({ name: "my-agent" });

    const agent = createAgent({
      checkpointer: checkpointSaver,
      // other configuration: model, system prompt, tools, etc.
    });

    this.on("invoke", async (req) => {
      const { threadId, content } = req.data;

      // Invoke the agent with just the latest user message
      const res = await agent.invoke(
        { messages: [{ role: "user", content }] },
        {
          configurable: {
            // Use a unique threadId per user to isolate conversation threads
            thread_id: `${threadId}-${req.user.id}`,
          },
        }
      );

      // Return the last message content from the agent
      return res.messages[res.messages.length - 1].content;
    });

    return super.init();
  }
}

A few important points:

  • You only pass the latest user message into the agent. You don’t manually maintain full history in your service code. Checkpointer handles that for you.
  • The thread_id inside configurable is the key that LangGraph uses to group checkpoints. Combining threadId with req.user.id gives you per‑user, per‑thread isolation.
  • The checkpointer persists each step of the graph for that thread_id into CAP’s persistence. When the next request comes in with the same threadId, the agent resumes with full context.

5. Invoke the agent and see durability in action

With the service deployed, you can start a conversation:

POST /rest/agent/invoke
Content-Type: application/json

{
  "threadId": "test-thread-1",
  "content": "Give me the list of all the books in the catalog."
}

Then, continue the conversation with the same threadId:

POST /rest/agent/invoke
Content-Type: application/json

{
  "threadId": "test-thread-1",
  "content": "What questions have I asked you so far?"
}

Even though you only send the latest user message, the agent can recall the previous turns because the checkpointer has been saving and loading the full state behind the scenes.

To test isolation, try a different threadId:

POST /rest/agent/invoke
Content-Type: application/json

{
  "threadId": "test-thread-2",
  "content": "What questions have I asked you so far?"
}

This should behave like a fresh conversation, since it uses a different thread and therefore a different checkpoint stream.

If you want to see a complete example, including the agent definition and tools, you can look at the LangChain example in the GitHub repo: examples/langchain-cds-persistence.


Practical best practices for durable agents on CAP

Once you have durability, a few patterns help keep things sane in production:

  • Use meaningful threadId values
    Treat threadId as a business‑level concept: a conversation, a workflow instance, a support ticket, etc. Don’t just generate random IDs with no semantic meaning.

  • Let CAP own multitenancy
    The package injects entities into your CDS model, and CAP handles tenant routing. Avoid building custom tenant switches inside your agent logic; rely on CAP’s standard patterns instead.

  • Plan checkpoint lifecycle
    Checkpoints will accumulate over time. You should periodically clean up old threads that are no longer active. For example:

    const staleThreads = await tx.run(
      SELECT.from(Checkpoints)
        .columns("graphName", "namespace", "threadId", "max(createdAt) as lastActivity")
        .groupBy("graphName", "namespace", "threadId")
        .having("max(createdAt) <", cutoffTime)
    );

    You can then delete checkpoints for those stale threads, or archive them depending on your compliance needs.

  • Expose entities for debugging (optionally)
    For troubleshooting and introspection, you can project the plugin’s entities into your own service:

    using plugin.langgraph.persistence as langgraph from '@mi8y/cds-langgraph-persistence';
    
    service CheckpointViewerService {
      entity Checkpoints as projection on langgraph.Checkpoints; // view all checkpoints
    }

    This is handy during development or incident analysis, but you don’t need to expose such viewers in production if you have other observability tooling.

Durability is just one piece of building production‑ready agents. You still need observability, evals, guardrails, and human‑in‑the‑loop patterns that fit your SAP landscape. The goal of @mi8y/cds-langgraph-persistence is to make the durability part boring and reliable. In the future blogs, I'll cover other aspects of taking agents to production.

Disclaimer: This article was written by me, but the narrative refined and type-checked by AI.
Edit 1: Added cds add langgraph-persistence to import the CDS entities

Labels in this area