---
title: "LangGraph"
slug: "langgraph"
updated: 2026-04-29T06:30:46Z
published: 2026-04-29T06:30:46Z
---

> ## Documentation Index
> Fetch the complete documentation index at: https://docs.plainid.io/llms.txt
> Use this file to discover all available pages before exploring further.

# LangGraph

*Early Access Capability*

          
  

PlainID integrates with LangGraph to enforce authorization across agent workflows, including prompt categorization, data anonymization, and Policy-based retrieval.

This integration enables you to apply centralized authorization decisions at each step of a stateful agent graph, ensuring that execution paths, data access, and outputs are governed by Policy.

The integration is built on top of:

- `core-plainid` for authorization and Policy evaluation.
- `langchain-plainid` for retrieval and filtering.

See the following articles for more information:

> [Integration Core](/v1/docs/integration-core) [LangChain](/v1/docs/langchain-agentic-ai)

## Install the SDK

Install the SDK to add the required PlainID and LangGraph integration packages.

**To install the SDK:**

1. Run the installation command.

```
pip install langgraph-plainid
```

This installs:

- `core-plainid`
- `langchain-plainid`
- `langgraph-plainid`

## Define Agent State

All nodes operate on a shared `AgentState` `TypedDict` that flows through the graph. The state contains the `request_context` for identity information and optional sub-states for each node type. Alternatively, `request_context` can be provided at construction time to the underlying components, for example, `PlainIDPermissionsProvider` and `FilterDirectiveProvider`. See `core-plainid`.

**To define Agent State:**

1. Import the required class.

```
from langgraph_plainid.models.state.agent_state import AgentState
```

1. Review the `AgentState` fields.

| Field | Type | Description |
| --- | --- | --- |
| request_context | RequestContext | Identity context (entity ID, type, additional identities) |
| categorization | CategorizationState | Sub-state with query and optional error_details |
| anonymization | AnonymizationState | Sub-state with query, optional output_text, and error_details |
| retrieval | RetrievalState | Sub-state with query, resource_types, optional retrieved_documents, and error_details |

The state typically includes:

- `request_context` for identity information.
- `categorization` for prompt classification input.
- `anonymization` for text processing input and output.
- `retrieval` for the query and retrieved documents.

## Initialize Permissions Provider

All PlainID nodes rely on the same permissions provider.

**To initialize the permissions provider:**

1. Import the required class.

```
from core_plainid.utils.plainid_permissions_provider import PlainIDPermissionsProvider
```

1. Configure the `PlainIDPermissionsProvider`.

```
permissions_provider = PlainIDPermissionsProvider(
    base_url="https://platform-product.us1.plainid.io",
    client_id="your_client_id",
    client_secret="your_client_secret",
)
```

## Add Categorization Node

The categorization node validates whether a prompt is allowed based on PlainID Policies.

**To add a Categorization Node:**

1. Import the required classes.

```
from core_plainid.categorization.categorizer import Categorizer
from langgraph_plainid.nodes.categorization_node import CategorizationNode
```

1. Create the `Categorizer`.

```
categorizer = Categorizer(
    classifier_provider=classifier,
    permissions_provider=permissions_provider,
    all_categories=["contract", "HR", "finance"],
)
```

1. Create the `CategorizationNode`.

```
categorization_node = CategorizationNode(
    categorizer=categorizer,
    next_node="anonymizer",
)
```

Note that `next_node` is optional.

## Add an Anonymization Node

The anonymization node applies Policy-driven masking or encryption to sensitive data.

**To add an Anonymization Node:**

1. Import the required classes.

```
from core_plainid.anonymization.presidio_anonymizer import PresidioAnonymizer
from langgraph_plainid.nodes.anonymizer_node import AnonymizerNode
```

1. Create the anonymizer.

```
anonymizer = PresidioAnonymizer(
    permissions_provider=permissions_provider,
    encrypt_key="your_16_char_key!",
)
```

1. Create the `AnonymizerNode`.

```
anonymizer_node = AnonymizerNode(
    anonymizer=anonymizer,
    next_node="retrieval",
)
```

This node:

- Reads from `state["anonymization"]["query"]`.
- Writes to `state["anonymization"]["output_text"]`.

## Add Retrieval Node

The retrieval node enforces PlainID filtering when querying vector stores.

**To add a Retrieval Node:**

1. Import the required classes.

```
from langchain_plainid.retrieval.filter_directive_provider import FilterDirectiveProvider
from langchain_plainid.retrieval.multi_store_retriever import MultiStoreRetriever
from langgraph_plainid.nodes.retrieval_node import RetrievalNode
```

1. Configure the `FilterDirectiveProvider`.

```
filter_provider = FilterDirectiveProvider(
    base_url="https://platform-product.us1.plainid.io",
    client_id="your_client_id",
    client_secret="your_client_secret",
)
```

1. Create the `MultiStoreRetriever`.

```
retriever = MultiStoreRetriever(
    filter_provider=filter_provider,
    resource_types=["customer"],
    vector_stores=[customer_store],
    k=4,
)
```

1. Create the `RetrievalNode`.

```
retrieval_node = RetrievalNode(retriever=retriever)
```

This node:

- Reads from `state["retrieval"]["query"]`.
- Writes to `state["retrieval"]["retrieved_documents"]`.

## Build Graph

Compose the nodes into a `StateGraph`.

**To build the graph:**

1. Import the required classes.

```
from langgraph.graph import START, END, StateGraph
from core_plainid.models.context.request_context import RequestContext
```

1. Create the graph.

```
graph = StateGraph(AgentState)
```

1. Add the nodes.

```
graph.add_node("categorization", categorization_node)
graph.add_node("anonymizer", anonymizer_node)
graph.add_node("retrieval", retrieval_node)
```

1. Add the edges.

```
graph.add_edge(START, "categorization")
graph.add_edge("categorization", "anonymizer")
graph.add_edge("anonymizer", "retrieval")
graph.add_edge("retrieval", END)
```

1. Compile the graph.

```
app = graph.compile()
```

## Execute Workflow

Invoke the graph with a Request Context and input queries.

**To execute the workflow:**

1. Create the `RequestContext`.

```
request_context = RequestContext(
    entity_id="your_entity_id",
    entity_type_id="your_entity_type",
    additional_identities=[
        AdditionalIdentity(
            entity_id="your_additional_entity_id",
            entity_type_id="your_additional_entity_type",
        ),
    ],
)
```

1. Invoke the graph.

```
result = await app.ainvoke({
    "request_context": request_context,
    "categorization": {"query": "What is John Smith's contract status?"},
    "anonymization": {"query": "What is John Smith's contract status?"},
    "retrieval": {"query": "What is John Smith's contract status?"},
})
```

1. Access the results.

```
print(result["anonymization"]["output_text"])
print(result["retrieval"]["retrieved_documents"])
```

## Routing Between Nodes

Nodes can define the next execution step using `next_node`.

**To route between nodes:**

1. Configure the `CategorizationNode` with `next_node`.

```
categorization_node = CategorizationNode(
    categorizer=categorizer,
    next_node="anonymizer",
)
```

1. Configure the `AnonymizerNode` with `next_node`.

```
anonymizer_node = AnonymizerNode(
    anonymizer=anonymizer,
    next_node="retrieval",
)
```

This allows the graph to dynamically control execution flow without explicitly defining all edges.

## Architecture Summary

The PlainID and LangGraph integration introduces a **Policy enforcement layer embedded into the agent graph execution model**. It includes a:

- **State Layer (`AgentState`)** A shared, structured state object carries identity, inputs, and outputs across all nodes.
- **Policy Decision Layer (PlainID)** Centralized authorization engine evaluates:
  - Prompt intent (categorization).
  - Data sensitivity (anonymization).
  - Data access (retrieval filters).
- **Execution Layer (LangGraph Nodes)** Each node enforces a specific control point:
  - **Categorization Node** → governs *what can be asked*.
  - **Anonymization Node** → governs *what data is exposed*.
  - **Retrieval Node** → governs *what data can be accessed*.
- **Flow Control Layer (Graph Orchestration)** LangGraph manages execution paths, while PlainID Policies influence:
  - Whether execution continues.
  - What transformations occur.
  - What data is returned.

## Error Handling

When `next_node_on_error` is set, errors are caught and the graph routes to the specified error handler node. The error details are available in the sub-state.

**To handle errors:**

1. Define the error handler.

```
def error_handler(state: AgentState) -> dict:
    for key in ["categorization", "anonymization", "retrieval"]:
        sub_state = state.get(key)
        
        if sub_state and sub_state.get("error_details"):
            error_details = sub_state["error_details"]
            
            print(f"Error in {key}: {error_details['error_message']}")
            print(f"Exception: {error_details['error']}")

    return {}
```

1. Add the error handler node to the graph.

```
graph.add_node("error_handler", error_handler)
```
