# Implementing Custom Content Transformations Using LangGraph in Open-Notebook

> Learn to implement custom content transformations with LangGraph in Open-Notebook. Discover a three-layer architecture for reusable LLM-powered text processing using Pydantic, LangGraph, and FastAPI.

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

---

**Open-Notebook implements custom content transformations through a three-layer architecture combining Pydantic domain models, LangGraph state machines, and FastAPI endpoints, allowing users to define reusable LLM-powered text transformations via declarative configuration.**

Open-Notebook is an open-source AI research assistant that leverages LangGraph to power server-side content transformations. Implementing custom content transformations using LangGraph enables developers to create reusable, prompt-driven text processing pipelines that integrate seamlessly with the application's SurrealDB-backed storage layer.

## Architecture Overview

The transformation system is organized into three tightly-coupled layers:

- **Domain Model** – Stores transformation metadata and prompt templates in [`open_notebook/domain/transformation.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/transformation.py)
- **Graph Definition** – Defines the LangGraph `StateGraph` that executes LLM operations in [`open_notebook/graphs/transformation.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/transformation.py)
- **API Surface** – Exposes CRUD and execution endpoints through [`api/routers/transformations.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/transformations.py)

This separation allows you to define new transformations by creating database records without modifying Python code, while the LangGraph engine handles the orchestration.

## Domain Model Layer

### The Transformation Model

The `Transformation` class in [`open_notebook/domain/transformation.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/transformation.py) is a Pydantic model that stores user-defined prompts and configuration:

```python
class Transformation(ObjectModel):
    table_name: ClassVar[str] = "transformation"
    name: str                 # short identifier, used in UI & API

    title: str                # human-readable label

    description: str
    prompt: str               # raw Jinja-style template

    apply_default: bool       # if true, prepend default instructions

```

Each record represents a reusable transformation template that the LangGraph pipeline can execute against any text input.

### Default Prompts Configuration

The `DefaultPrompts` singleton in the same file holds site-wide transformation instructions. When `apply_default` is `true`, the system automatically prepends these instructions to your custom prompt, ensuring consistent formatting or persona across all transformations.

## LangGraph State Machine

### State Definition

The graph operates on a `TransformationState` TypedDict defined in [`open_notebook/graphs/transformation.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/transformation.py):

```python
class TransformationState(TypedDict):
    input_text: str               # optional raw text to transform

    source: Source                # optional SurrealDB Source object

    transformation: Transformation
    output: str

```

This state object carries the configuration and content through the graph nodes.

### Graph Execution Flow

The transformation graph is a minimal `StateGraph` containing a single execution node (`run_transformation`). The node logic performs four critical operations:

1. **Content Selection** – Falls back to `source.full_text` when `input_text` is missing
2. **Prompt Assembly** – Combines the stored `transformation.prompt` with `DefaultPrompts.transformation_instructions` (if enabled) and adds an `# INPUT` marker

3. **LLM Provisioning** – Calls `provision_langchain_model` from [`open_notebook/ai/provision.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/ai/provision.py) with the assembled system prompt and `HumanMessage`
4. **Response Handling** – Extracts plain text, strips "thinking" artifacts using `clean_thinking_content`, and persists the result as an insight on the source

The compiled graph callable supports asynchronous invocation with a configurable `model_id` passed through the graph configuration.

## API Integration

The [`api/routers/transformations.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/transformations.py) router provides the complete CRUD lifecycle plus an execution endpoint. The `execute` method bridges HTTP requests to the LangGraph pipeline:

```python
@router.post("/transformations/execute", response_model=TransformationExecuteResponse)
async def execute_transformation(execute_request: TransformationExecuteRequest):
    transformation = await Transformation.get(execute_request.transformation_id)
    model = await Model.get(execute_request.model_id)

    result = await transformation_graph.ainvoke(
        dict(input_text=execute_request.input_text,
             transformation=transformation),
        config=dict(configurable={"model_id": execute_request.model_id}),
    )
    return TransformationExecuteResponse(
        output=result["output"],
        transformation_id=execute_request.transformation_id,
        model_id=execute_request.model_id,
    )

```

This endpoint validates the transformation and model entities, then forwards the request to `transformation_graph.ainvoke()`. The response contains the LLM-generated output, and if a source object was provided, the system automatically stores the result as an insight in SurrealDB.

## Creating a Custom Transformation

You can implement new transformations entirely through the REST API without restarting the application:

**1. Define the transformation record:**

```json
POST /transformations
{
    "name": "summarize",
    "title": "Summarize Text",
    "description": "Produces a concise summary of the input.",
    "prompt": "Summarize the following text in 3 bullet points:",
    "apply_default": true
}

```

**2. (Optional) Update global default instructions:**

```json
PUT /transformations/default-prompt
{
    "transformation_instructions": "You are a helpful assistant."
}

```

**3. Execute the transformation:**

```json
POST /transformations/execute
{
    "transformation_id": "<uuid-from-step-1>",
    "model_id": "gpt-4o-mini",
    "input_text": "Open-Notebook is an AI-powered research assistant..."
}

```

The response contains the transformed output and persists the result as an insight if a source object was included in the request.

## Extending the Graph

For complex pipelines requiring multiple nodes or conditional branching, modify [`open_notebook/graphs/transformation.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/transformation.py):

```python

# Add a logging node after the main transformation

async def log_output(state: dict, config: RunnableConfig) -> dict:
    print("Transformation result:", state["output"])
    return state

agent_state.add_node("logger", log_output)
agent_state.add_edge("agent", "logger")
agent_state.add_edge("logger", END)

```

You can add conditional edges using `add_conditional_edges` to route based on LLM confidence scores or metadata flags. All modifications preserve the same state interface, ensuring compatibility with the existing API router.

## Code Examples

### Python Client Example

```python
import httpx

BASE = "http://localhost:5055"

# Create the transformation

resp = httpx.post(
    f"{BASE}/transformations",
    json={
        "name": "extract_keywords",
        "title": "Extract Keywords",
        "description": "Pulls out the most important keywords.",
        "prompt": "List the top 5 keywords from the text:",
        "apply_default": False,
    },
)
transformation_id = resp.json()["id"]

# Run it

run_resp = httpx.post(
    f"{BASE}/transformations/execute",
    json={
        "transformation_id": transformation_id,
        "model_id": "gpt-4o-mini",
        "input_text": "LangGraph enables composable LLM workflows...",
    },
)
print("Output:", run_resp.json()["output"])

```

### cURL Example

```bash

# Create transformation

curl -X POST http://localhost:5055/transformations \
  -H "Content-Type: application/json" \
  -d '{
        "name":"title_case",
        "title":"Title-Case Converter",
        "description":"Converts a sentence to title case.",
        "prompt":"Convert the following text to title case:",
        "apply_default":true
      }' | jq .id

# Execute transformation

curl -X POST http://localhost:5055/transformations/execute \
  -H "Content-Type: application/json" \
  -d '{
        "transformation_id":"<id-from-above>",
        "model_id":"gpt-4o-mini",
        "input_text":"open-notebook makes AI research easy"
      }' | jq .output

```

## Summary

- **Three-layer architecture** separates concerns between data models ([`open_notebook/domain/transformation.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/transformation.py)), graph logic ([`open_notebook/graphs/transformation.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/transformation.py)), and HTTP interfaces ([`api/routers/transformations.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/transformations.py))
- **Declarative configuration** allows new transformations via JSON API calls without code deployment
- **LangGraph state machine** handles prompt assembly, LLM invocation via `provision_langchain_model`, and output sanitization through `clean_thinking_content`
- **Automatic persistence** stores transformation results as insights when source objects are provided
- **Extensible design** supports custom graph nodes and conditional edges for complex pipelines

## Frequently Asked Questions

### How do I add a new transformation without modifying the source code?

Create a new record via the `POST /transformations` endpoint with your custom prompt template. The LangGraph engine in [`open_notebook/graphs/transformation.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/transformation.py) dynamically loads these records at runtime, requiring no application restarts or code changes.

### What happens if I provide both `input_text` and a `source` object?

The `run_transformation` node in [`open_notebook/graphs/transformation.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/transformation.py) prioritizes `input_text` when present, falling back to `source.full_text` only when the input text is missing. This allows you to override source content with custom text for specific transformations.

### Can I use different LLM models for different transformations?

Yes. The `execute` endpoint accepts a `model_id` parameter that routes through `provision_langchain_model` in [`open_notebook/ai/provision.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/ai/provision.py). Pass any configured model ID (e.g., `"gpt-4o-mini"`, `"claude-3-opus"`) to use model-specific behavior for individual transformation calls.

### How do I remove "thinking" artifacts from LLM outputs?

The system automatically calls `clean_thinking_content` from [`open_notebook/utils/clean_thinking_content.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/clean_thinking_content.py) before persisting insights. This function strips reasoning chains and internal monologue markers, ensuring only the final transformed content is stored in the database.