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.
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.
The Functional API uses `@entrypoint` for the durable workflow and `@task` for discrete work whose result should be recorded. Keep non-deterministic calls and external side effects inside tasks. When execution resumes, recorded task results can be reused, but the entrypoint control flow must follow a compatible sequence so the runtime can match prior work.
Idempotency still matters. A task may be retried or re-executed after a failure boundary, and a remote call may complete even when its response is lost. Give writes a stable operation ID, check for an existing result, and return the committed outcome. Do not place a payment or email directly in untracked entrypoint code.
A subgraph deserves a clear input, output, state mapping, checkpointer policy, and failure contract. Per-invocation persistence fits independent calls; per-thread persistence supports an agent that retains state across calls but cannot be invoked concurrently without careful namespace design. Inspect subgraph state during debugging instead of flattening every internal channel into the parent.
Replay-safe functional code must make the same task decisions from recorded input and state. Avoid current-time, random, or environment-dependent branching outside tasks unless those values are first captured durably. Parallel tasks may complete in a different wall-clock order, so combine their results by stable key rather than accidental completion order.
Choose the API at module boundaries, not by personal preference across the whole codebase. An explicit parent graph can call a functional workflow, and a functional entrypoint can invoke a compiled subgraph. Keep the boundary typed and traceable so composition does not hide ownership.
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.