LangGraph Plugin for Temporal Python SDK
Release stage: Public Preview.
This Temporal Plugin allows you to run LangGraph nodes and tasks as Temporal Activities, giving your AI workflows durable execution, automatic retries, and timeouts. It supports both the LangGraph Graph API (StateGraph) and Functional API (@entrypoint / @task).
Installation
uv add temporalio-langgraph
Plugin Initialization
Graph API
from langgraph.graph import StateGraph
from temporalio.langgraph import LangGraphPlugin
g = StateGraph(State)
g.add_node("my_node", my_node, metadata={"execute_in": "activity"})
plugin = LangGraphPlugin(graphs={"my-graph": g})
Functional API
from temporalio.langgraph import LangGraphPlugin
plugin = LangGraphPlugin(
entrypoints={"my_entrypoint": my_entrypoint},
tasks=[my_task],
activity_options={"my_task": {"execute_in": "activity"}},
)
Checkpointer
If your LangGraph code requires a checkpointer (for example, if you're using interrupts), use InMemorySaver.
Temporal handles durability, so third-party checkpointers (like PostgreSQL or Redis) are not needed.
import langgraph.checkpoint.memory
import typing
from temporalio.langgraph import graph
from temporalio import workflow
@workflow.defn
class MyWorkflow:
@workflow.run
async def run(self, input: str) -> typing.Any:
g = graph("my-graph").compile(
checkpointer=langgraph.checkpoint.memory.InMemorySaver(),
)
...
Execution Location
Every node (Graph API) and task (Functional API) must be labeled with execute_in, set to either "activity" or "workflow". This is required per node/task; it cannot be set in default_activity_options.
# Graph API
graph.add_node("my_node", my_node, metadata={"execute_in": "activity"})
graph.add_node("tool_node", tool_node, metadata={"execute_in": "workflow"})
# Functional API
plugin = LangGraphPlugin(
tasks=[my_task, tool_task],
activity_options={
"my_task": {"execute_in": "activity"},
"tool_task": {"execute_in": "workflow"},
},
)
Activity Options
Options are passed through to workflow.execute_activity(), which supports parameters like start_to_close_timeout, retry_policy, schedule_to_close_timeout, heartbeat_timeout, and more.
Graph API
Pass Activity options as node metadata when calling add_node:
from datetime import timedelta
from temporalio.common import RetryPolicy
g = StateGraph(State)
g.add_node("my_node", my_node, metadata={
"execute_in": "activity",
"start_to_close_timeout": timedelta(seconds=30),
"retry_policy": RetryPolicy(maximum_attempts=3),
})
Functional API
Pass Activity options to the LangGraphPlugin constructor, keyed by task function name:
from datetime import timedelta
from temporalio.common import RetryPolicy
from temporalio.langgraph import LangGraphPlugin
plugin = LangGraphPlugin(
entrypoints={"my_entrypoint": my_entrypoint},
tasks=[my_task],
activity_options={
"my_task": {
"execute_in": "activity",
"start_to_close_timeout": timedelta(seconds=30),
"retry_policy": RetryPolicy(maximum_attempts=3),
},
},
)
Runtime Context
LangGraph's run-scoped context (context_schema) is reconstructed on the Activity side, so nodes and tasks can read from and write to runtime.context:
from langgraph.runtime import Runtime
from typing_extensions import TypedDict
from temporalio.langgraph import graph
class Context(TypedDict):
user_id: str
async def my_node(state: State, runtime: Runtime[Context]) -> dict:
return {"user": runtime.context["user_id"]}
# In the Workflow:
g = graph("my-graph").compile()
await g.ainvoke({...}, context=Context(user_id="alice"))
Your context object must be serializable by the configured Temporal payload converter, since it crosses the Activity boundary.
Summaries
Summaries are short, human-readable labels that show up in the Temporal UI and CLI, making it easier to see what each step of a run is doing.
Static summary
summary is an ordinary Activity option, so a fixed per-node label works today — pass it like any other option:
g.add_node("plan", plan, metadata={"execute_in": "activity", "summary": "Planning step"})
It is attached to the node's scheduled-activity event (execute_in="activity" only).
Dynamic summary (summary_fn)
To derive the label from the node's input at runtime, supply a summary_fn. It receives the node's (args, kwargs) and returns a summary string, or None/"" for no summary. For a StateGraph node args[0] is the state; for a Functional @task it is the task's arguments.
def summarize(args, kwargs) -> str | None:
state = args[0]
return f"stage={state['stage']} doc={state['doc_id']}"
# Graph API: per-node
g.add_node("plan", plan, metadata={"execute_in": "activity", "summary_fn": summarize})
# Functional API: per-task
plugin = LangGraphPlugin(
tasks=[plan],
activity_options={"plan": {"execute_in": "activity", "summary_fn": summarize}},
)
summary_fn is set per node/task (like the static summary), so different nodes — which receive different inputs — can compute their summaries independently. You can also put a summary or summary_fn in default_activity_options as a fallback for every node; a node/task that sets either form overrides the inherited default (you just can't set both forms at the same level).
- For
execute_in="activity"nodes the result sets the activitysummary(one per scheduled-activity event, visible in history). - For
execute_in="workflow"nodes there is no activity, so the result updates the workflow's current details viaworkflow.set_current_details(). This is a single workflow-level slot (last-writer-wins) reflecting the most recent workflow-bound node that defines asummary_fn; aNone/""result clears it. It is queryable via__temporal_workflow_metadata.
summary_fn runs in workflow context on every replay, so it must be deterministic and must not raise (an exception fails the workflow task). Setting both a static summary and a summary_fn on the same node raises ValueError.
Streaming
When streaming_topic is set on LangGraphPlugin, calls to langgraph.config.get_stream_writer() inside a node publish to the named topic on the workflow's WorkflowStream. Activity-side nodes publish via WorkflowStreamClient (a signal carrying batched items, controlled by streaming_batch_interval); workflow-side nodes publish synchronously to the in-workflow stream (no signal). External subscribers consume the stream with WorkflowStreamClient.create(...).topic(...).subscribe(...).
The workflow must construct WorkflowStream() in its @workflow.init (i.e. __init__)
from datetime import timedelta
from typing import Any
from langgraph.config import get_stream_writer
from langgraph.graph import START, StateGraph
from typing_extensions import TypedDict
from temporalio import workflow
from temporalio.client import Client
from temporalio.langgraph import LangGraphPlugin, graph
from temporalio.contrib.workflow_streams import WorkflowStream, WorkflowStreamClient
from temporalio.worker import Worker
class State(TypedDict):
value: str
async def token_node(state: State) -> dict[str, str]:
writer = get_stream_writer()
for token in ["hello", " ", "world"]:
writer({"token": token})
writer({"done": True})
return {"value": "hello world"}
@workflow.defn
class StreamingWorkflow:
def __init__(self) -> None:
# Required when streaming_topic is set on the plugin.
_ = WorkflowStream()
self.app = graph("streaming").compile()
@workflow.run
async def run(self) -> str:
result = await self.app.ainvoke({"value": ""})
return result["value"]
async def main(client: Client) -> None:
g = StateGraph(State)
g.add_node("token_node", token_node, metadata={"execute_in": "activity"})
g.add_edge(START, "token_node")
async with Worker(
client,
task_queue="streaming-tq",
workflows=[StreamingWorkflow],
plugins=[
LangGraphPlugin(
graphs={"streaming": g},
default_activity_options={
"start_to_close_timeout": timedelta(seconds=10)
},
streaming_topic="tokens",
)
],
):
handle = await client.start_workflow(
StreamingWorkflow.run, id="streaming-wf", task_queue="streaming-tq"
)
ws_client = WorkflowStreamClient.create(client, handle.id)
async for item in ws_client.topic("tokens", type=dict).subscribe(from_offset=0):
print(item.data)
if item.data.get("done"):
break
print(await handle.result())
What's covered, and what isn't
streaming_topic wires up exactly one LangGraph stream mode: stream_mode="custom", i.e. values written through get_stream_writer(). The other modes — "messages", "values", "updates", "debug" — are not captured by streaming_topic. They aren't produced by node-side writers; LangGraph's orchestrator emits them as it walks the graph. The documented pattern is to bridge astream() in the workflow and republish each yielded chunk to a WorkflowStream topic yourself:
@workflow.defn
class AstreamBridge:
def __init__(self) -> None:
self.stream = WorkflowStream()
self.app = graph("g").compile()
@workflow.run
async def run(self) -> None:
topic = self.stream.topic("astream")
async for chunk in self.app.astream({...}, stream_mode="messages"):
topic.publish(chunk)
topic.publish({"done": True})
Retry semantics
Streaming has at-least-once delivery per activity attempt. When an activity-wrapped node retries (transient failure, worker crash, etc.), the user function re-runs from scratch and re-publishes its writes — earlier publishes from the failed attempt are not rolled back. Subscribers should be ready to see duplicates and recover idempotently (e.g. dedupe on a sequence id you include in each chunk, or treat the stream as advisory and rely on the workflow's final result for state).
Tracing
We recommend the Temporal LangSmith Plugin to trace your LangGraph Workflows and Activities.
Stores are not supported
LangGraph's Store (e.g. InMemoryStore passed via graph.compile(store=...) or @entrypoint(store=...)) isn't accessible inside Activity-wrapped nodes: the Store holds live state that can't cross the Activity boundary, and Activities may run on a different worker than the Workflow. If you pass a store, the plugin logs a warning on first use and runtime.store is None inside nodes.
Use Workflow state for per-run memory, or an external database (Postgres/Redis/etc.) configured on each worker if you need shared memory across runs.
Running Tests
Install dependencies:
make sync
Run the test suite:
make test
Tests start a local Temporal dev server automatically — no external server needed.
Metadata
Release files for temporalio-langgraph 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| temporalio_langgraph-0.1.0.tar.gz | 17.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| temporalio_langgraph-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 37.8 kB
Release files / temporalio_langgraph-0.1.0.tar.gz
| Download URL | temporalio_langgraph-0.1.0.tar.gz |
|---|---|
| Size | 17.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
35d67361016052b7a87e3878096ca9cd77a7390979dbb60e5b5bb32b53098e0d
|
|
BLAKE2b-256 checksum How to use checksums |
a00321d4cbeda7843c276d01aea5fe6d17bd82877ac4111f29b39975e568cb6e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency logRelease files / temporalio_langgraph-0.1.0-py3-none-any.whl
| Download URL | temporalio_langgraph-0.1.0-py3-none-any.whl |
|---|---|
| Size | 20.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7a1643d089559d70cf3982eea9628810dc0981a1bc599f376d66a2dc69a3779e
|
|
BLAKE2b-256 checksum How to use checksums |
e02baf6582b850ce59ca84d491c24a92d0bfaa6eb0ea02a73e15df6e66af6b73
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency log