LangGraph provides two complementary ways to express orchestration. The Graph API makes state, nodes, and edges explicit. The Functional API lets ordinary Python control flow become durable by decorating an entrypoint and the operations that must be recorded as tasks.
Neither API is universally better. The Graph API is strongest when workflow topology should be visible and inspectable. The Functional API is attractive when the process reads more naturally as conditionals, loops, and function calls.
Subgraphs add composition. A parent graph can delegate a bounded workflow to another graph while preserving clear state and persistence rules. This is useful for reusable specialists, domain modules, and multi-agent systems.
Think of the Graph API as a visible workflow map and the Functional API as durable Python control flow. Tasks record unstable work; subgraphs package reusable workflows.
Choose the Graph API when named nodes, routes, visualization, and state transitions are central to understanding the business process. Choose the Functional API when the workflow is easier to read as normal Python and you still need persistence, streaming, interrupts, and durable execution.
The two APIs can coexist. A Functional API entrypoint can call graph-backed components, and a graph node can delegate work to a function or subgraph.
An `@entrypoint` function represents the workflow invocation boundary. Its input and return value form the public contract, while a checkpointer can persist progress for a thread.
Keep entrypoint inputs serializable and explicit. Hidden global state makes replay, testing, and production recovery harder to reason about.
Durable execution may replay workflow code from an earlier point when a run resumes. Operations such as API calls, random values, timestamps, file writes, and payments should therefore be wrapped in tasks or isolated nodes.
Recorded task results can be reused during replay instead of repeating completed work. Write operations should still be idempotent because a task that started but failed before recording completion may run again.
A subgraph is a compiled graph used inside another graph. It is a good fit when one part of the system has its own state transitions, tests, ownership, or reuse across several parent workflows.
State can be shared directly when parent and child use common keys, or mapped through a wrapper node when their schemas differ. Explicit mapping is often safer for independently owned modules.
Stateful subgraphs can inherit the parent checkpointer, which allows checkpointing, interrupts, and state inspection to work across the composed execution.
When a subgraph pauses or fails, resumption may replay the parent node that invoked it as well as work inside the child. That makes side-effect isolation and idempotency important at both levels.
A subgraph boundary should make deployment, testing, tracing, permissions, or team ownership clearer. Splitting every node into a subgraph adds ceremony without improving control.
Trace parent and child names distinctly so operators can see whether a failure came from routing, state mapping, or the child workflow itself.
Choose the Functional API when the workflow is easiest to understand as ordinary Python control flow but still needs durable execution. The entrypoint describes the main workflow, and tasks wrap work that may need checkpointing, replay awareness, retries, or safe side-effect handling. This is especially useful for sequential workflows that would look awkward if forced into many tiny graph nodes.
Choose subgraphs when a part of the workflow deserves its own internal structure. A research workflow, approval workflow, or specialist-agent workflow may have enough state, routes, and tests to justify a subgraph. The parent graph should not need to know every internal step; it should only know the subgraph contract: input state, output update, failure behavior, and trace boundary.
The expert decision is not Graph API versus Functional API versus subgraph forever. Many systems combine them. Use explicit graphs where architecture review matters, functional entrypoints where Python-shaped orchestration is clearer, and subgraphs where composition improves ownership, testing, or reuse.
This workflow records the external lookup as a task while keeping the orchestration in normal Python control flow.
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.func import entrypoint, task
@task
def lookup_order(order_id: str) -> dict:
# External API call belongs inside a task.
return {"order_id": order_id, "status": "shipped"}
@entrypoint(checkpointer=InMemorySaver())
def order_status_workflow(request: dict) -> dict:
order = lookup_order(request["order_id"]).result()
return {
"answer": f"Order {order['order_id']} is {order['status']}.",
"order": order,
}
config = {"configurable": {"thread_id": "order-17"}}
print(order_status_workflow.invoke({"order_id": "ORD-17"}, config))
A stable idempotency key prevents duplicate side effects if work must be retried.
from langgraph.func import task
@task
def create_ticket(run_id: str, summary: str) -> dict:
idempotency_key = f"{run_id}:create-ticket"
# Pass the key to the backend so a retry returns the existing ticket.
return {
"ticket_id": "INC-1042",
"idempotency_key": idempotency_key,
"summary": summary,
}
The child graph owns research-specific state while a wrapper maps data across the boundary.
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
class ResearchState(TypedDict):
query: str
findings: list[str]
def search(state: ResearchState) -> dict:
return {"findings": [f"Result for {state['query']}"]}
research_builder = StateGraph(ResearchState)
research_builder.add_node("search", search)
research_builder.add_edge(START, "search")
research_builder.add_edge("search", END)
research_graph = research_builder.compile()
class ParentState(TypedDict):
question: str
evidence: list[str]
def run_research(state: ParentState) -> dict:
child_result = research_graph.invoke({
"query": state["question"],
"findings": [],
})
return {"evidence": child_result["findings"]}
parent_builder = StateGraph(ParentState)
parent_builder.add_node("research", run_research)
parent_builder.add_edge(START, "research")
parent_builder.add_edge("research", END)
parent_graph = parent_builder.compile()
Yes. They are complementary orchestration styles and can call shared functions or graph-backed components.
No. Tasks improve durable replay, but writes should still use idempotency because failures can occur before completion is recorded.
Often it can inherit the parent checkpointer. Use separate persistence only when the ownership and lifecycle genuinely require it.
Explore 500+ free tutorials across 20+ languages and frameworks.