The entrypoint and task decorators: the same durable runtime this book built Atlas on, described in plain functions instead of an explicit graph.
Every chapter in this book builds Atlas as a StateGraph - nodes, edges, an explicit state schema. LangGraph ships a second way to describe a durable workflow: two decorators, @entrypoint and @task, that let you write a plain Python function with ordinary loops and conditionals and get the same checkpointing, retries, and durable execution underneath. This appendix exists because it is a different authoring style worth knowing, not because Atlas needed it. Nothing in the main chapters uses it, and the reasons why are the point of the “when imperative reads cleaner” section below.
The two decorators
The listing folds per-turn fact extraction into a running list. It is an illustration of the decorators, not Chapter 14’s reflect, which makes one extractor call over the whole transcript and then compacts. The companion’s atlas/functional_variant.py defines local, deterministic stand-ins for extractor and judge_model, so both listings here run offline.
atlas/functional_variant.py
from typing import TypedDictfrom langgraph.checkpoint.memory import InMemorySaverfrom langgraph.func import entrypoint, task# ...from langgraph.types import RetryPolicy# ...@task(retry_policy=RetryPolicy(max_attempts=3)) # a bare @task never retriesdef extract_facts(turn: str) ->list[dict]:"""One unit of work: its result is saved when it finishes."""return extractor.invoke(turn)@entrypoint(checkpointer=InMemorySaver())def collect_facts( turns: list[str], *, previous: list[dict] |None=None) ->list[dict]:"""Per-turn extraction, folded into the thread's running list. `previous` is the last invocation's return value on this thread.""" all_facts =list(previous or []) futures = [extract_facts(turn) for turn in turns] # tasks run concurrentlyfor future in futures: all_facts.extend(future.result())return all_facts
@task wraps a function as a unit of work. Calling it returns a future, not a result, and it can only be called while a run is executing: from an @entrypoint, from a StateGraph node, or from another task. Called bare, it raises RuntimeError.
A task takes the same retry_policy= and cache_policy= a graph node takes (Chapters 4 and 8), passed on the decorator. A bare @task has neither: it fails on the first exception. A task’s cache is also ignored unless the entrypoint is given cache=, the same trap as a node cache without compile(cache=...).
previous is the return value of the last invocation on the same thread_id, and it exists only with a checkpointer; without one it is always None, whatever thread_id you pass. It is the functional API’s version of the state a thread carries between turns (Chapter 9). Here it grows on every call, which is the append-only log Chapter 14’s compaction exists to prevent.
The same runtime underneath
@entrypoint/@task are not a separate execution engine competing with StateGraph. Both compile onto the same Pregel-derived runtime this book has been describing since Chapter 3. The entrypoint compiles to a graph with one node, so the whole function is one superstep, with a checkpoint at its boundary. Inside it, each task’s result is saved as the task finishes.
That makes the task the resumption boundary. If the process running collect_facts is killed after two of its four tasks finished, and the thread lives on a durable checkpointer such as Postgres, re-invoking the same thread with None as the input re-runs only the two that did not, and the finished results come back from the checkpoint. The listing’s InMemorySaver dies with the process, so after a real kill it leaves nothing to resume.
An ordinary exception leaves less to redo. A task that raises does not stop its siblings: the runner lets every in-flight task finish and saves its result, so the resume re-runs only the task that raised. A RetryPolicy still decides whether a failed task runs again; thread_id still scopes what persists. The functional API changes how you describe the workflow, not what guarantees the runtime gives you once it’s running.
Figure 1 follows collect_facts through that crash and the resume, on a durable checkpointer.
Figure 1: Tasks as the resume unit inside one entrypoint. In run 1, collect_facts starts four extract_facts tasks in one superstep; two finish and their results are saved, and the process is killed while turns 3 and 4 are still in flight. On a durable checkpointer, invoke(None) on the same thread reads the two saved results back without re-running them and runs only turns 3 and 4, and the entrypoint returns. That return value is what the next invocation on the thread receives as previous, which is None without a checkpointer.
One interaction to avoid: max_concurrency, the bound Chapter 17 puts on a fan-out. At this book’s pinned version, an entrypoint that started four tasks under max_concurrency of 1 or 2 hung, and the same run under 4 or 8 finished. Leave the cap off an entrypoint’s tasks, or set it at least as wide as the fan-out.
Figure 2 lines the two styles up, concept by concept.
Figure 2: The Graph API and the Functional API as two ways to write the same runtime concepts. A node becomes a @task, the compiled graph an @entrypoint, edges become if and for, and channels plus a checkpointer become previous. Retry and cache take the same policies on the decorator, but a bare @task has neither, and a task’s cache does nothing without the entrypoint’s cache=. An entrypoint mounts as a node, and at this book’s pin a max_concurrency of 1 or 2 hung an entrypoint’s four tasks.
Mixing functional tasks inside a graph
A @task is usable directly inside a StateGraph node, which is the practical interop point: a node can call a @task-wrapped function to get individually retried, individually cached sub-units of work without restructuring the whole node as its own graph. On a checkpointer, a crashed node’s finished tasks are not repeated when the thread resumes.
atlas/functional_variant.py
@taskdef score_one_candidate(candidate: dict) ->float:return judge_model.invoke(candidate)class RankState(TypedDict):"""Illustrative only - 'candidates'/'ranked' are not real Atlas state channels; see the appendix's own caveat on `rank_candidates`.""" candidates: list[dict] ranked: list[dict]def rank_candidates(state: RankState) ->dict:"""A StateGraph node using @task for its own internal retry/cache granularity - Ch17's Send would be the alternative if this needed the runtime's own parallel-fan-out bookkeeping instead.""" candidates = state["candidates"] futures = [score_one_candidate(c) for c in candidates] scores = [f.result() for f in futures] ranked = [ cfor _, c insorted(zip(scores, candidates), key=lambda pair: pair[0], reverse=True ) ]return {"ranked": ranked}
RankState is not Atlas’s state: candidates and ranked exist only for this example. The sort key is the score alone, so two candidates with the same score never fall through to comparing dicts.
The reverse works too. An @entrypoint compiles to the same kind of graph a StateGraph does, so it mounts as a node, or is invoked from inside one, under Chapter 17’s subgraph rules: shared keys flow in and out, and anything else needs a wrapper.
When imperative reads cleaner
Chapter 14’s reflection pass is close to the functional API’s sweet spot: extract, check, compact, in a line, with nothing a reader needs a diagram to follow. Written as tasks under an entrypoint, it would read the way the chapter’s own prose describes it.
Atlas’s main resolve flow is the opposite case. Chapter 6 branches on triage, Chapter 10 crosses a checkpoint membrane with compensation, Chapter 11 suspends for approval - real, multi-way control flow where the shape of the graph is itself something a reviewer needs to see to reason about routing correctness (Chapter 6’s whole argument: control flow belongs on edges, visible, not buried in a function body). An imperative rewrite of atlas/graph.py would bury exactly the routing boundary Chapter 6 spent a chapter making explicit - back inside if/elif branches a reviewer has to read start to finish to reconstruct, instead of a graph they can look at.
Reach for the functional API when the workflow is a pipeline a reader would draw as a straight line with one or two forks. Reach for StateGraph the moment the routing itself - not just the work being done - is what needs to be reviewable.