* Finalize draft for 0037-standalone-activities-go * Update Standalone Activities documentation for Go SDK Edits throughout Go standalone activities --------- Co-authored-by: skill-sync[bot] <skill-sync[bot]@users.noreply.github.com> Co-authored-by: Donald Pinckney <donald.pinckney@temporal.io>
8.2 KiB
Temporal LangGraph Plugin (Python)
Overview
The LangGraphPlugin runs LangGraph nodes and tasks as Temporal Activities, giving LangGraph-orchestrated AI workflows durable execution, automatic retries, and timeouts.
Both LangGraph APIs are supported:
- Graph API (
StateGraph). - Functional API (
@entrypoint/@task).
The plugin ships in the Temporal Python SDK at temporalio.contrib.langgraph.
Note
This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview.
For general Temporal Python AI/LLM patterns (Pydantic data converter, LLM Activity design, retry classification, multi-agent orchestration), read references/python/ai-patterns.md first; for language-agnostic patterns, read references/core/ai-patterns.md.
Installation
uv add temporalio[langgraph]
This pulls in langgraph as well for you.
Public API
temporalio.contrib.langgraph exports four symbols:
LangGraphPlugin— the plugin class. Pass to both theClientand theWorker.graph(name, cache=...)— Workflow-side accessor that returns a registeredStateGraphby name.entrypoint(name, cache=...)— Workflow-side accessor that returns a registered Functional-APIPregel.cache()— accessor for the per-workflow LangGraph cache.
Build the plugin
Construct the plugin once at startup. The constructor signature is:
LangGraphPlugin(
graphs: dict[str, StateGraph] | None = None,
entrypoints: dict[str, Pregel] | None = None,
tasks: list | None = None,
activity_options: dict[str, dict[str, Any]] | None = None,
default_activity_options: dict[str, Any] | None = None,
)
Graph API
Register your StateGraph instances in graphs=, keyed by a name your workflow code will look up with graph(name):
from langgraph.graph import StateGraph
from temporalio.contrib.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
Pass entrypoints in entrypoints=, tasks in tasks=, and per-task activity options in activity_options= (keyed by task function name):
from temporalio.contrib.langgraph import LangGraphPlugin
plugin = LangGraphPlugin(
entrypoints={"my_entrypoint": my_entrypoint},
tasks=[my_task],
activity_options={"my_task": {"execute_in": "activity"}},
)
Register the plugin on Client and Worker
Pass the same LangGraphPlugin instance to both the Temporal Client and the Worker via their plugins= parameter, like any other Temporal Python SDK plugin:
from temporalio.client import Client
from temporalio.worker import Worker
client = await Client.connect("localhost:7233", plugins=[plugin])
worker = Worker(
client,
task_queue="my-task-queue",
workflows=[MyWorkflow],
plugins=[plugin],
)
await worker.run()
Execution location — required per node and per task
Every node (Graph API) and every task (Functional API) must declare execute_in, set to either "activity" or "workflow":
# Graph API — set via node metadata
graph.add_node("my_node", my_node, metadata={"execute_in": "activity"})
graph.add_node("tool_node", tool_node, metadata={"execute_in": "workflow"})
# Functional API — set via activity_options on the plugin
plugin = LangGraphPlugin(
tasks=[my_task, tool_task],
activity_options={
"my_task": {"execute_in": "activity"},
"tool_task": {"execute_in": "workflow"},
},
)
- Use
"activity"for I/O, LLM calls, or anything that needs Temporal retries and timeouts. - Use
"workflow"for logic that should run on the workflow thread: purely deterministic logic, or logic which orchestrates Temporal durable primitives within the workflow thread (e.g. multiple activity calls, wait conditions, etc.) execute_incannot be defaulted indefault_activity_options— it must be specified individually per node or task.
Activity options
For nodes or tasks with execute_in: "activity", you can pass parameters that flow through to workflow.execute_activity(): start_to_close_timeout, retry_policy, schedule_to_close_timeout, heartbeat_timeout.
Graph API — options in node metadata
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 — options keyed by task name
from datetime import timedelta
from temporalio.common import RetryPolicy
from temporalio.contrib.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),
},
},
)
Run a graph from a Workflow
Inside the Workflow, get the registered graph by name with graph(name), compile it, then ainvoke (or astream) as usual. If the graph requires a checkpointer (for example, when using interrupts), use InMemorySaver — Temporal supplies durability, so third-party checkpointers like PostgreSQL or Redis are not needed:
import typing
import langgraph.checkpoint.memory
from temporalio import workflow
from temporalio.contrib.langgraph import graph
@workflow.defn
class MyWorkflow:
@workflow.run
async def run(self, input: str) -> typing.Any:
g = graph("my-graph").compile(
checkpointer=langgraph.checkpoint.memory.InMemorySaver(),
)
return await g.ainvoke({"input": input})
Use entrypoint(name) instead of graph(name) for Functional-API entrypoints.
Runtime context
LangGraph's run-scoped context (context_schema) is reconstructed on the Activity side, so a node can read and write runtime.context even when the node runs as an Activity:
from langgraph.runtime import Runtime
from typing_extensions import TypedDict
from temporalio.contrib.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"))
The context object must be serializable by the configured Temporal payload converter, since it crosses the Activity boundary. If your context uses Pydantic models, configure pydantic_data_converter — see references/python/ai-patterns.md.
Tracing
For LangSmith tracing of LangGraph nodes and Temporal Activities together, use the Temporal LangSmith plugin (references/python/integrations/langsmith.md).
Hard constraints
execute_inis mandatory on every node and task. Set it to"activity"or"workflow"per node/task — it cannot be set indefault_activity_options.- Use
InMemorySaveras your checkpointer. Temporal handles durability; do not configure PostgreSQL, Redis, or other third-party checkpointers. - LangGraph
Storeis not supported across the Activity boundary. If you pass aStore(e.g.InMemoryStoreviagraph.compile(store=...)or@entrypoint(store=...)), the plugin logs a warning on first use andruntime.storeisNoneinside nodes. Use Workflow state for per-run memory, or an external database (Postgres/Redis/etc.) configured on each worker for shared memory across runs. - Context objects must be serializable by the configured Temporal payload converter, since they cross the Activity boundary.
Resources
references/python/ai-patterns.md— Python AI/LLM patterns (Pydantic data converter, LLM Activity design, retry/error classification).references/core/ai-patterns.md— language-agnostic AI/LLM patterns.references/python/integrations/langsmith.md- Companion LangSmith plugin.