Checkpointers and threads
A thread is the unit a user recognizes as “my run,” a checkpoint is one superstep of it, and the thread_id in the config is the single value your entire URL and routing design hangs off.
A broker opens HouseWarm, uploads a mortgage offer, gets three fields back for review, and then her phone rings. She closes the tab. Twenty minutes later she wants to finish, and she wants to finish from her laptop rather than the machine she started on.
Today that request has no answer, because there was never a run to come back to. The extraction was a request and a response. The review gate held state in a form. Once the tab closed, the only durable thing left was whatever had already been written to your database, and the run itself was never a thing your database knew about.
A checkpointed graph changes the shape of that problem completely. The run becomes an addressable object with an identity, a history, and a position. This lesson is about that identity, because it turns out to be the single value your URL scheme, your routing, your share links and your access control all hang off.
Two definitions, and one of them becomes a URL
LangChain’s checkpointers documentation defines both terms directly. A checkpoint is “a snapshot of the graph state saved at each super-step and is represented by a StateSnapshot object.” A thread is “a unique ID or thread identifier assigned to each checkpoint saved by a checkpointer. It contains the accumulated state of a sequence of runs.” LangChain writes this documentation and sells the hosted platform it leads to, so read the mechanics as authoritative and the enthusiasm as a vendor’s.
Note the relationship. A checkpoint is one superstep. A thread is the sequence, and it accumulates across runs, plural — so a thread is not one invocation. Invoke the same graph twice with the same thread, and the second invocation starts from where the first one stopped.
That is the sentence to sit with, because it is the whole product feature. The interrupts documentation puts the consequence in a form you can hand to an engineer:
“The thread_id you choose is effectively your persistent cursor. Reusing it resumes the same checkpoint; using a new value starts a brand-new thread with an empty state.” (LangChain, Interrupts)You pass it in the config, nested under configurable:
const config = { configurable: { thread_id: "user-42-run-7" } };
const snapshot = await graph.getState(config); // -> StateSnapshotSo the identifier is chosen by the caller, not minted by the runtime. Every consequence you care about follows from that. A thread id that embeds a user is guessable across users. A thread id generated fresh on every page load silently discards the run the person was in the middle of. A thread id you cannot reconstruct from a URL means a run nobody can link to, which means no share, no email, no second reviewer, no deep-link back from a notification. This is a routing decision, and it is yours.
The snapshot is the whole read API
There are two reads. getState(config) returns the latest snapshot for a thread. getStateHistory(config) returns the thread’s snapshots, most recent first. Both hand back the same object, and its seven fields are the entire vocabulary a “where is this run” screen has to work with:
| Field | Holds | The screen question it answers |
|---|---|---|
values | State channel values at this checkpoint | What does the run know so far |
next | Node names to execute next. An empty [] means done | Is it finished, and if not, what happens when it moves |
config | thread_id, checkpoint_ns, checkpoint_id | The handle you pass back to address this exact point |
metadata | source, writes, step | How far in, and what produced this checkpoint |
createdAt | ISO 8601 timestamp | “Paused 20 minutes ago” — and whether that is alarming |
parentConfig | The previous checkpoint’s config, null for the first | Where this point came from, one step back |
tasks | PregelTask[]: id, name, error, interrupts, optional state | What is pending, what failed, and what is waiting on a human |
Field names on this list are camelCase because this is the JavaScript documentation. The Python side of the same object uses created_at and parent_config. If you are writing a TypeScript client against a graph somebody else runs in Python, that difference is a real source of undefined-shaped bugs, and it is one of several the TypeScript-side-of-the-wire lesson comes back to.
Asking whether a thread is waiting on a person
A paused run and a finished run both look, to a naive client, like a run that is not currently streaming anything. Telling them apart is the documented idiom, and it reads through tasks:
const snapshot = await graph.getState(config);
const isWaitingOnHuman =
snapshot.tasks.length > 0 && snapshot.tasks.some((t) => t.interrupts.length > 0);Two conditions, because they are two different facts. There being tasks at all means the run has somewhere left to go. Some task carrying interrupts means what it is waiting for is a human answer rather than the next superstep.
Each entry in a task’s interrupts array is the shape the stream-modes lesson already showed you under __interrupt__: { id, value }, where value is exactly what the node passed to interrupt(). Which means the question you asked the user is readable from a cold snapshot, by someone who was never watching the stream. That is what makes a shareable review link possible at all.
It also means “is this thread waiting on me” is a query you can run across many threads, which is how you get an inbox. A queue of pending reviews is not a feature the runtime provides. It is this predicate, run over the threads a person is allowed to see.
Where checkpoints actually live
The checkpointer is a swappable backend. The in-memory saver, MemorySaver, ships with @langchain/langgraph-checkpoint and is bundled, which is why every tutorial uses it and why every tutorial is therefore demonstrating a system that forgets everything when the process restarts. The durable ones are separate installs: @langchain/langgraph-checkpoint-sqlite (SqliteSaver), -postgres (PostgresSaver), -mongodb (MongoDBSaver) and -redis (RedisSaver).
This course does not print the constructor call for any of them. The package names are documented; the connection-string and setup calls were not on any LangChain page fetched during this course’s research passes on 2026-09-02, 2026-09-03 or 2026-09-05, and the npm registry returned 403 for the package README. Copy that from the version you have installed, not from here and not from a blog post.
What you do need from this, as the person designing the surface, is one question to ask on day one: which saver is wired up. A team demoing a resumable review flow on MemorySaver has built a demo, not a feature, and the difference will not be visible in any recording.
Check your recall
Answer from memory — no scrolling back.
Retrieval check
Design the URL for a HouseWarm review. What goes in the path, and name one thing that must not.
Check your answer
The thread id goes in the path, because it is the only handle that addresses the run — something like /reviews/[threadId]. That is what makes the page linkable, refreshable, and openable by a colleague, which are three separate product requirements that all collapse into the same routing decision.
What must not go in it is anything you derive the id from which also grants access. A thread id shaped like broker-14-doc-9 is enumerable: it tells anyone holding one URL how to construct the next one. The runtime does not authorize anything — possession of a thread id is possession of a handle to the run, and the entire access check is your application’s. Use an opaque id, and check on every read that this user is allowed this thread.
The checkpoint id is the other half of the answer, and it belongs in a query parameter rather than the path, because it is a position within the thread rather than the thread itself. You do not need it yet. You will the moment anyone wants to look at an earlier point, which is the time-travel-and-forking lesson.
Hands on
Give the parser run an address
Done when: ARTIFACT.md’s module 3 section names the thread_id scheme for the parser, the route that carries it, the access check that guards it, and a written answer to “which saver is wired up” — plus the three run states expressed as a predicate over real snapshot fields.
- Decide what one run of the parser is, in the product’s own words. One document? One document per broker? A whole submission of six documents? The thread is that unit, and choosing it wrong is expensive later, because a thread accumulates across runs and you cannot un-merge two things you gave one id.
- Write the
thread_idscheme down, and write beside it how a client reconstructs it after a reload. If the answer involves anything held only in memory, the scheme does not work. - Write the route. Then write the sentence describing what stops somebody else’s thread id from working in it. The runtime will not do this for you.
- Implement the three-state predicate against a real snapshot: finished, waiting on a human, still working. Use
nextandtasks, not a status field you invented. If you find yourself wanting a fourth state, write down what it is — that is usually the point where a failed task is being confused with a paused one. - Run the graph, close the process, start it again, and call
getStatewith the same config. Paste what comes back. If it is empty, you are on the in-memory saver, and now you know that from your own terminal rather than from a claim in a lesson. - Bring the scheme into the chat. I will look for an enumerable thread id and for a run screen that cannot tell “done” from “waiting.”
What this does not cover
This lesson gave the run an address and a set of fields to read. It did not do the reading. Turning a snapshot into a screen that a person who walked away twenty minutes ago can orient themselves inside is a different job, and most of it is deciding what to say about next and metadata.step in words a broker understands. That is the resuming-a-run-they-walked-away-from lesson.
Nothing here touched the history. getStateHistory appeared as a name and then got dropped, which badly undersells it: the thread keeps every checkpoint, so earlier points are addressable, replayable and forkable. What each of those two verbs actually does to a run, and why one of them costs money, is the time-travel-and-forking lesson.
And this lesson has been carefully silent about what is inside values. A checkpoint holds whatever the state schema declares, which on a document parser includes the document. That is the state-you-can-show lesson, and it is the one to read before anyone outside the product team asks where the mortgage offer went.
Read this next — primary source
CheckpointersLangChain — docs.langchain.com, JavaScript docs, fetched 2026-09-05. Vendor documenting its own product, and the docs lead to the hosted platform it sells
This lesson takes the two definitions, the full StateSnapshot field list, and the documented way to ask whether a thread is waiting on a person. The page goes further than the lesson does: it enumerates the storage backends, and it is the reference to keep open when you are deciding what your own read endpoints return. Read the field list slowly. Every one of those seven fields answers a question a screen will eventually ask.
Stuck, curious, or think this lesson is wrong? Ask your teaching agent. The lessons are the scaffold; the conversation is where the learning gets unstuck.