@entrypoint / @task: functional API
Responsibilities
@entrypoint and @task are the two cornerstones of LangGraph's functional API, defined in libs/langgraph/langgraph/func/__init__.py. @entrypoint wraps an ordinary Python function (sync or async) as a Pregel instance — the return value is a compiled graph that can be .invoked / .astreamed directly (entrypoint.__call__:516). @task wraps a function as a _TaskFunction (_TaskFunction:59); calling it returns a SyncAsyncFuture, and it can only be called inside an @entrypoint or a StateGraph node.
It sits above StateGraph as an alternative upper-layer entrypoint — no need to build a StateGraph, call add_node / add_edge / compile; just write a function. The underlying engine is still Pregel: entrypoint.__call__ ultimately does return Pregel(...), wrapping the entrypoint function as a single-node graph (func.__name__ is the only node name, the trigger is START); the node executes the entrypoint function body, and when the function returns it writes the return value into the END channel (Pregel construction:572).
Design motivation
- Less boilerplate:
StateGraphrequires explicitly declaring a state schema, node functions, edges, and conditional edges.@entrypointlets you write a single function; the sequence oftaskcalls inside the body is the graph structure. For simple workflows (a few calls, chaining a couple of tools) the functional API is lighter. - Preserves all of Pregel: the functional API is not a stripped-down version — the underlying engine is
Pregel, socheckpointer,store,cache,retry_policy,context_schema,interrupt(),Command(resume=...)all work. Callinginterrupt()inside an entrypoint function goes through the same mechanism as calling it inside aStateGraphnode. entrypoint.finaldecouples return value from persisted value: many conversational workflows need "return X to the caller this turn, but persist Y into the checkpoint for next time" (thepreviousparameter reads the last save).entrypoint.final(value=X, save=Y)(entrypoint.final:477) explicitly splits these two concerns, instead of relying ondictconventions.previousparameter model: the entrypoint function signature may include*, previous: Any = None; the next call with the samethread_idreceives the value saved last time, equivalent to a "cross-call memory variable" without declaring a state schema and reducer.- Task calls are graph edges: inside an entrypoint body
task_a(...)returns a future;.result()/awaitretrieves the result — this call relationship is naturally an edge. Pregel treats it as a PUSH task pushed into theTASKSchannel; the nextprepare_next_tasksschedules it. Noadd_edgeneeded.
Key files
_TaskFunction:59— product of the@taskdecorator; holdsfunc/retry_policy/cache_policy/timeout._TaskFunction.__call__:86— when a task is called, routes through_call_with_options, which pulls theCONFIG_KEY_CALLcallback from config and hands the task to the current runner.task decorator:110— entry point of the@taskdecorator; supports both@taskand@task(retry_policy=...)forms.entrypoint class:262— the@entrypointdecorator class;finalis attached as a nested dataclass on the class.entrypoint.__init__:437— acceptscheckpointer/store/cache/retry_policy/timeout/context_schemaand stores them onself.entrypoint.final:477—final(value=R, save=S)dataclass; returnsvalueto the caller, persistssaveto the checkpoint.entrypoint.__call__:516— turns the function into aPregelinstance; the actual conversion logic lives here.Pregel construction:572— constructsPregel(nodes={func.__name__: PregelNode(bound=bound, triggers=[START], ...)}); the channels areSTART/END/PREVIOUS, allLastValue.get_runnable_for_entrypoint:175— wraps the entrypoint function as aRunnableCallable; the sync version goes throughrun_in_executor.get_runnable_for_task:200— wraps the task function asRunnableSeq(run, ChannelWrite([RETURN])); the task return value is written to theRETURNchannel.
Data flow
The core of @entrypoint turning a function into Pregel is constructing a minimal graph with only three channels:
graph: Pregel[Any, ContextT, Any, Any] = Pregel(
nodes={
func.__name__: PregelNode(
bound=bound,
triggers=[START],
channels=START,
timeout=self.timeout,
writers=[
ChannelWrite(
[
ChannelWriteEntry(END, mapper=_pluck_return_value),
ChannelWriteEntry(PREVIOUS, mapper=_pluck_save_value),
]
)
],
)
},
channels={
START: EphemeralValue(input_type),
END: LastValue(output_type, END),
PREVIOUS: LastValue(save_type, PREVIOUS),
},
input_channels=START,
output_channels=END,
stream_channels=END,
stream_mode=stream_mode,
stream_eager=True,
checkpointer=self.checkpointer,
store=self.store,
cache=self.cache,
# ...
)START is an EphemeralValue (reset at the start of each step); END and PREVIOUS are both LastValue (keep only the latest). The entrypoint function's return value is split by two mappers: _pluck_return_value writes entrypoint.final.value (or the bare value) to END for the caller to read; _pluck_save_value writes entrypoint.final.save (or the bare value) to PREVIOUS as the next call's previous (pluck mappers:547). stream_mode="updates" is the default for the functional API (not values), because an entrypoint may contain multiple tasks and updates better reflects intermediate progress.
Task call chain: task_fn(arg) → _TaskFunction.__call__ → _call_with_options (_call_with_options:276) → reads impl from config[CONF][CONFIG_KEY_CALL] → impl(func, args, kwargs, retry_policy=..., callbacks=..., timeout=...) returns a SyncAsyncFuture. This impl is injected by PregelRunner.tick / atick via partial(...) while scheduling tasks (CONFIG_KEY_CALL inject:211) — so a task does not run itself; it hands its function and arguments to the current runner, which decides when to run it concurrently and returns the result via a future.
Boundaries and failure modes
- Generators not supported: the entrypoint function cannot be
def gen/async def gen(withyield);__call__explicitlyraise NotImplementedErrorat the top (no generators:525). For streaming output useStreamWriter(stream_mode="custom") instead ofyield. - Tasks cannot be called externally: calling
task_fn(...)from outside an@entrypoint/StateGraphnode fails, becauseget_config()[CONF][CONFIG_KEY_CALL]does not exist. A task's existence depends on the current runtime runner context. - Sync tasks cannot take a timeout:
task(timeout=...)only works for async functions; a sync function raisessync_timeout_unsupported(sync timeout unsupported:237) — a sync task runs in an executor thread, and Python has no safe way to cancel it in-process. previousdefaults to None: on the first callpreviousisNone; the entrypoint function must handle the "no previous value" branch itself. Ifentrypoint.finalis not used, the return value is written to bothENDandPREVIOUS, so the next call'spreviousreceives the previous return value itself.finalmust be properly parameterized: the return type annotation-> entrypoint.final[int, str]must provide both type parameters; providing only one or none raisesTypeErrorin__call__(final param check:562).- Depends on a checkpointer: when
entrypoint(checkpointer=...)is not configured with a checkpointer, neitherinterrupt()norpreviouscross-call memory works — the former raises RuntimeError, the latter isNoneevery time.
Summary
The functional API is syntactic sugar over StateGraph; underneath it is still a Pregel single-node graph + the TASKS channel + a runner-injected CONFIG_KEY_CALL. The sequence of task(...) calls inside the entrypoint body is the implicit graph structure. For concurrency details see /func/concurrency; for interrupt/resume see /interrupt/interrupt. See official docs: LangGraph · README.