# How LangGraph Orchestrates Chat Workflows with Message History Persistence in Open Notebook

> Learn how LangGraph orchestrates chat workflows using state graphs and SQLite for message history persistence in Open Notebook. Ensure data integrity across API calls and restarts.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: how-to-guide
- Published: 2026-06-28

---

**Open Notebook uses LangGraph to model chat conversations as state graphs with SQLite-backed checkpointing, ensuring every message persists across API calls and server restarts while maintaining a stateless HTTP interface.**

Open Notebook leverages LangGraph to transform ephemeral LLM interactions into persistent, stateful conversations. By combining typed state management with automatic SQLite checkpointing, the application enables users to resume chat sessions across devices without losing context. This architecture centers on a declarative graph defined in [`open_notebook/graphs/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/chat.py) that orchestrates message flow and history retention.

## Defining Typed Chat State with ThreadState

At the core of the workflow is the `ThreadState` class, a `TypedDict` that defines the schema for mutable data passing between graph nodes. Located in [`open_notebook/graphs/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/chat.py), this state container holds the complete context of a conversation:

```python
class ThreadState(TypedDict):
    messages: Annotated[list, add_messages]   # list of LangChain messages

    notebook: Optional[Notebook]               # notebook the chat belongs to

    context: Optional[str]                     # optional extra context

    context_config: Optional[dict]             # config for context building

    model_override: Optional[str]              # explicit model selection

```

The `messages` field uses LangGraph's `add_messages` reducer to automatically append new messages rather than replace the entire list. This typed definition ensures that every node in the graph receives a predictable state shape, enabling type-safe access to conversation history and metadata.

## Persistent Checkpointing with SQLite

Message history survives process restarts through LangGraph's `SqliteSaver` checkpoint mechanism. The configuration in [`open_notebook/config.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/config.py) defines `LANGGRAPH_CHECKPOINT_FILE`, which specifies the SQLite database path used for persistence. When the graph compiles, this saver automatically serializes the complete `ThreadState` after every node execution:

```python

# From open_notebook/graphs/chat.py

memory = SqliteSaver.from_conn_string(checkpoint_path)
graph = agent_state.compile(checkpointer=memory)

```

This checkpoint layer stores the full message list, notebook references, and context configurations to disk. When a user reconnects to an existing chat session, LangGraph reloads the previous state from SQLite, allowing the conversation to continue exactly where it left off.

## Building the LangGraph Workflow

The chat graph follows a simple yet extensible linear flow. The implementation in [`open_notebook/graphs/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/chat.py) constructs a `StateGraph` using the `ThreadState` type, adding a single processing node:

```python
agent_state = StateGraph(ThreadState)
agent_state.add_node("agent", call_model_with_messages)
agent_state.add_edge(START, "agent")
agent_state.add_edge("agent", END)
graph = agent_state.compile(checkpointer=memory)

```

The graph begins at `START`, routes to the `"agent"` node, and terminates at `END`. The compiled graph binds the SQLite checkpointer, ensuring that state mutations persist automatically whenever the agent node completes execution.

## Executing LLM Calls in Graph Nodes

The `"agent"` node executes `call_model_with_messages`, which handles the actual LLM interaction. This function retrieves the `Prompter` template *chat/system* to construct the system prompt, then provisions the appropriate LangChain model via `open_notebook.ai.provision.provision_langchain_model`. 

Because the graph nodes run synchronously but model provisioning may require async operations, the function manages event loop handling internally. After receiving the LLM response, it processes the output through `extract_text_content` and `clean_thinking_content` to remove any `<thinking>` tags before returning the cleaned `AIMessage`. The updated `messages` list flows back into the graph state, where the checkpointer automatically persists the appended message to SQLite.

## Linking Sessions to Checkpoints

While LangGraph handles conversation state, Open Notebook manages session metadata through the `ChatSession` domain model in [`open_notebook/domain/notebook.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/notebook.py). When a client creates a new chat session via the API, the system stores session metadata in SurrealDB and associates the LangGraph checkpoint file with that session's unique identifier.

This separation of concerns allows the application to maintain user-facing session records in a document database while LangGraph manages the transactional message history in SQLite. The session ID serves as the key to retrieve the correct checkpoint state on subsequent API calls.

## HTTP API Integration

The FastAPI router in [`api/routers/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/chat.py) exposes the stateful graph through a stateless HTTP interface. Incoming requests forward to `ChatService`, which invokes the compiled graph using `graph.ainvoke()`:

```python

# From api/chat_service.py (conceptual)

async def handle_chat(session_id: str, user_msg: dict):
    state = await graph.ainvoke(
        {"messages": [Message(**user_msg)], "notebook": notebook},
        config={"configurable": {"model_id": "gpt-4o"}}
    )
    return state["messages"]

```

The service passes the `model_id` through the configurable graph parameters, allowing dynamic model selection per request. The checkpointer automatically loads previous messages using the session ID, appends the new user message and AI response, and persists the updated state before returning the complete message history to the client.

## Summary

- **Open Notebook** uses LangGraph to model chat interactions as a state graph with explicit nodes and edges defined in [`open_notebook/graphs/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/chat.py).
- The `ThreadState` TypedDict defines the schema for messages, notebook context, and model overrides, using `add_messages` to handle list mutations.
- **SQLite checkpointing** via `SqliteSaver` persists the complete state after every node execution, ensuring message history survives server restarts.
- The single `"agent"` node executes `call_model_with_messages`, which handles prompt templating, async model provisioning, and message cleaning.
- **Session management** separates concerns between SurrealDB (for `ChatSession` metadata) and SQLite (for LangGraph checkpoints), linked by session ID.
- The FastAPI layer provides a stateless HTTP interface while internally leveraging `graph.ainvoke()` to maintain persistent conversational state.

## Frequently Asked Questions

### How does LangGraph ensure chat messages persist between API calls?

LangGraph uses a `SqliteSaver` checkpointer configured via `LANGGRAPH_CHECKPOINT_FILE` in [`open_notebook/config.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/config.py). After each graph node executes, the saver automatically writes the complete `ThreadState`—including the full message list—to a SQLite database. When subsequent API calls reference the same session ID, LangGraph reloads the checkpointed state, allowing the conversation to resume with full context intact.

### What is the role of the ThreadState TypedDict in the chat workflow?

`ThreadState` serves as the immutable contract for data flowing through the graph. Defined in [`open_notebook/graphs/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/chat.py), it declares that each state contains a `messages` list (managed by the `add_messages` reducer), an optional `Notebook` reference, and configuration fields. This typing ensures that nodes like `call_model_with_messages` receive predictable inputs and return properly structured state updates.

### How does Open Notebook handle asynchronous LLM calls within LangGraph's synchronous node structure?

The `call_model_with_messages` function manages async model provisioning by running `open_notebook.ai.provision.provision_langchain_model` inside a fresh event loop or thread pool. This approach keeps the graph node synchronous-compatible while still allowing async LLM operations to complete. The function then extracts and cleans the response text before returning the updated state to the graph.

### Can users switch language models mid-conversation in Open Notebook?

Yes, the `model_override` field in `ThreadState` and the `configurable` parameters passed to `graph.ainvoke()` allow dynamic model selection per request. The API layer in [`api/routers/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/chat.py) forwards the requested `model_id` through the graph configuration, enabling users to override the default model for specific messages while maintaining the same persistent conversation history.