Custom encryption support for LangGraph.
.. warning:: This API is in beta and may change in future versions.
This module provides a framework for implementing custom at-rest encryption in LangGraph applications. Similar to the Auth system, it allows developers to define custom encryption and decryption handlers that are executed server-side.
Warning for beta features in LangGraph SDK.
Raised when attempting to register a duplicate encryption/decryption handler.
Encryption and decryption types for LangGraph.
This module defines the core types used for custom at-rest encryption in LangGraph. It includes context types and typed dictionaries for encryption operations.
Add custom at-rest encryption to your LangGraph application.
.. warning:: This API is in beta and may change in future versions.
The Encryption class provides a system for implementing custom encryption of data at rest in LangGraph applications. It supports encryption of both opaque blobs (like checkpoints) and structured JSON data (like metadata, context, kwargs, values, etc.).
To use, create a separate Python file and add the path to the file to your
LangGraph API configuration file (langgraph.json). Within that file, create
an instance of the Encryption class and register encryption and decryption
handlers as needed.
Example langgraph.json file:
{
"dependencies": ["."],
"graphs": {
"agent": "./my_agent/agent.py:graph"
},
"env": ".env",
"encryption": {
"path": "./encryption.py:my_encryption"
}
}
Then the LangGraph server will load your encryption file and use it to encrypt/decrypt data at rest.
JSON encryptors must not add or remove keys from the input dict. Only values may be transformed. This constraint is enforced at runtime by the server and exists because SQL JSONB merge operations (used for partial updates) work at the key level.
Correct (per-key encryption):
# Input: {"secret": "value", "plain": "x"}
# Output: {"secret": "<encrypted>", "plain": "x"} ✓ Keys preserved
Incorrect (key consolidation):
# Input: {"secret": "value", "plain": "x"}
# Output: {"__encrypted__": "<blob>", "plain": "x"} ✗ Key changed
If your encryptor needs to store auxiliary data (DEK, IV, etc.), embed it within the encrypted value itself, not as separate keys.
from langgraph_sdk import Encryption, EncryptionContext
my_encryption = Encryption()
SKIP_FIELDS = {"tenant_id", "owner", "thread_id", "assistant_id"}
ENCRYPTED_PREFIX = "encrypted:"
@my_encryption.encrypt.blob
async def encrypt_blob(ctx: EncryptionContext, blob: bytes) -> bytes:
return your_encrypt_bytes(blob)
@my_encryption.decrypt.blob
async def decrypt_blob(ctx: EncryptionContext, blob: bytes) -> bytes:
return your_decrypt_bytes(blob)
@my_encryption.encrypt.json
async def encrypt_json(ctx: EncryptionContext, data: dict) -> dict:
result = {}
for k, v in data.items():
if k in SKIP_FIELDS or v is None:
result[k] = v
else:
result[k] = ENCRYPTED_PREFIX + your_encrypt_string(v)
return result
@my_encryption.decrypt.json
async def decrypt_json(ctx: EncryptionContext, data: dict) -> dict:
result = {}
for k, v in data.items():
if isinstance(v, str) and v.startswith(ENCRYPTED_PREFIX):
result[k] = your_decrypt_string(v[len(ENCRYPTED_PREFIX):])
else:
result[k] = v
return resultThe ctx.model and ctx.field attributes tell you which model type and
specific field is being encrypted, allowing different logic:
@my_encryption.encrypt.json
async def encrypt_json(ctx: EncryptionContext, data: dict) -> dict:
if ctx.field == "metadata":
# Metadata - standard encryption
return encrypt_standard(data)
elif ctx.field == "values":
# Thread values - more sensitive, use stronger encryption
return encrypt_sensitive(data)
else:
return encrypt_standard(data)
Data encrypted with one (model, field) pair is not guaranteed
to be decrypted with the same pair. The server performs SQL JSONB
merges that can move encrypted values between models (e.g., cron
metadata → run metadata). Your decryption logic must handle data
regardless of the ctx.model or ctx.field values at decrypt time.
Safe: Use ctx.model/ctx.field for logging or metrics only.
Safe: Encrypt different keys based on ctx.field, but use a
single decrypt handler that decrypts any value with the encrypted
prefix (and passes through plaintext unchanged):
ENCRYPTED_PREFIX = "enc:"
@my_encryption.encrypt.json
async def encrypt_json(ctx: EncryptionContext, data: dict) -> dict:
# Encrypt different keys depending on the field
if ctx.field == "context":
keys_to_encrypt = {"api_key", "secret_token"}
else:
keys_to_encrypt = {"email", "ssn"}
return {
k: ENCRYPTED_PREFIX + encrypt(v) if k in keys_to_encrypt else v
for k, v in data.items()
}
@my_encryption.decrypt.json
async def decrypt_json(ctx: EncryptionContext, data: dict) -> dict:
# Decrypt ANY value with the prefix, regardless of model/field
return {
k: decrypt(v[len(ENCRYPTED_PREFIX):])
if isinstance(v, str) and v.startswith(ENCRYPTED_PREFIX)
else v
for k, v in data.items()
}
Unsafe: Using different encryption keys or algorithms based on
ctx.model/ctx.field will cause decryption failures.