State you can show, and state you cannot
Checkpointed state is a durable copy of everything the agent saw, including the document contents and the model’s reasoning about them — deciding what reaches the browser is a design decision, not a serialization detail.
Somebody from the platform team asks a simple question about your review feature: where does the mortgage offer live now?
The answer, once the parser is a checkpointed graph, is longer than it used to be. The document is in object storage, as before. Its extracted text is in the state schema, so it is also in the checkpoint store. It is in every checkpoint of the thread, because a checkpoint is written at each superstep. And it is readable by anything holding the thread id, in whatever shape the schema declared, indefinitely.
None of that is a bug and none of it is hidden. It follows from decisions somebody made in a state schema, possibly without noticing they were making them. This lesson is about noticing.
The checkpoint is the schema, exactly
A snapshot’s values field is documented as “State channel values at this checkpoint.” That is the whole rule, and the precision matters in both directions.
The checkpointer does not inspect the run, decide what was interesting, or capture everything the agent saw. It persists the channels the schema declares. So a durable copy of the document’s contents exists if and only if a channel holds the document’s contents. It is a schema decision that produces the durable copy, not a property of checkpointing.
In practice that distinction collapses fast, because agent state schemas accumulate channels the way any shared object does. A node needs the raw OCR text, so it goes in state. Another node wants the model’s reasoning about which field is which, so that goes in state. A third needs the page images to draw crops, so those go in too. Nobody added “persist the whole document forever” to a ticket. It is the sum of four reasonable local decisions.
Which is why the state table the state-is-the-contract lesson had you build carries a column that looks bureaucratic and is not: does this reach the browser. That column is now doing two jobs, because there are two different questions and they have different answers.
Two cuts, not one
Every channel gets classified twice, and conflating the two is the mistake.
- Is it checkpointed? A schema question. A channel declared with
UntrackedValueis live during execution and never written to a checkpoint. Everything else is durable, at every superstep, for the life of the thread. - Does it reach the browser? A serialization question, answered entirely by code you write. It has no relationship to the first one.
That gives four combinations and all four are real. Checkpointed and shown: the extracted fields. Checkpointed and not shown: the raw OCR text, which the run needs and a reviewer does not. Untracked and shown: a page crop, live during the run and fetched from storage after a resume. Untracked and not shown: an intermediate cache.
Notice which combination costs you nothing to get wrong in development. The second one. A channel that is checkpointed and should not be shown behaves identically to a safe channel right up until the moment it is serialized into a response, and there is no test that fails.
Nothing narrows this for you
The important claim in this lesson is an absence, so here is the search behind it. LangChain’s checkpointers page and its persistence page were both fetched in full during this course’s research passes on 2026-09-02 and again on 2026-09-05, read specifically for a filtering, redaction, projection or masking mechanism. Neither page documents one. getState and getStateHistory return the channels as they are.
So the narrowing is a component of yours, and it belongs on the server. Concretely: a function from StateSnapshot to a view model the client is allowed to hold. Not a client-side filter, which is a filter that has already lost, because the data reached the browser to be filtered. Not a decision made per-endpoint, which is a decision that will be made three different ways.
This is a place where your existing instincts transfer directly. It is the same discipline as never returning a user row from an ORM straight out of an API. The only new thing is that the object in question is richer than a database row, changes shape as the run proceeds, and has a copy at every step.
Where people get burned
History multiplies the exposure. A single leaked field is not one field, it is that field at every checkpoint of the thread, reachable through getStateHistory. And if you shipped the history view from the time-travel-and-forking lesson, you built the surface that serves them. Apply the projection to every snapshot the history endpoint returns, not only to the latest one.
What a broker should actually see
This section is this course’s own reasoning about its own product. LangChain has no opinion about what a client renders, and does not claim to.
For HouseWarm’s review step, a defensible cut looks roughly like this, and the interesting column is the last one.
| Channel | Checkpointed | Reaches the browser | Why |
|---|---|---|---|
| Extracted fields, with confidence | Yes | Yes | It is the thing being reviewed. Resume depends on it. |
| Raw OCR text of the whole document | Yes | The span behind the field under review, not the document | A reviewer needs to check one value against its source, which is a span. Shipping the whole text is convenience, and it is the document. |
| Model reasoning about field identification | Yes | No | Useful for debugging and for an audit trail. Not a thing to put in front of a broker who will read it as the system’s confidence in itself. |
| Page images | No, untracked | Yes, fetched from storage | Rendering the crop needs them. Persisting them at every superstep buys nothing and duplicates the document into a second store. |
Two of those rows are arguments rather than facts, and that is the point of the exercise. Whether a reviewer should see the model’s reasoning is a genuine product debate with a case on both sides. What is not defensible is not having had it, and discovering the answer by reading a network response.
The OCR row is the one worth arguing hardest, because it is where convenience and exposure look identical in code. Sending the span behind the field under review and sending the whole document’s text are one property access apart, and the second is the document.
Check your recall
Answer from memory — no scrolling back.
Retrieval check
A backend engineer proposes adding a `documentText` channel so a later node can re-read the source. It is the shortest path and it works. What do you say?
Check your answer
Say yes to the requirement and interrogate the storage. The node needs the text at the moment it runs. Putting it in a channel gets it a durable copy at every superstep of the thread, readable by anything holding the thread id, for as long as the thread exists. Those are two different asks, and only the first one was the requirement.
Offer the alternatives in order of cost. A reference — a document id the node resolves against storage — keeps the checkpoint small and leaves the access check where it already is. An UntrackedValue channel keeps the text live for the run without checkpointing it, at the price that a resumed run has to fetch it again. A plain checkpointed channel is the right answer when the text genuinely has to survive a resume without a second fetch, and that is a case worth stating rather than assuming.
Then, whichever wins, write the projection row before the channel exists. The failure mode here is not a wrong decision, it is an undocumented one: six months later a history view ships, nobody remembers that documentText was internal, and it is in a response.
Hands on
Cut the state, and defend the cut
Done when: ARTIFACT.md’s state table has both columns filled for every channel — checkpointed yes or no, reaches the browser yes or no — a server-side projection function exists and is applied to both the current-state and history reads, and one row carries a written argument you could defend to somebody outside the product team.
- Take the state table from the state-is-the-contract lesson and split the reaches-the-browser column into the two questions: checkpointed, and shown. Any row where those two were the same answer by default is a row nobody decided.
- Print the full
valuesof a real snapshot from your parser run and read it as though you were the broker. Everything in there is one carelessres.jsonaway from her screen. Mark what should not be. - Write one projection function, on the server: snapshot in, view model out, with the allowed channels named explicitly rather than the disallowed ones removed. An allowlist stays correct when somebody adds a channel; a denylist does not.
- Apply it to the history read as well as the current-state read. Then confirm it by fetching your own history endpoint and searching the response for a string from a channel you excluded.
- Pick the row you are least sure about — the model’s reasoning is the usual candidate — and write the paragraph arguing your call. Include what you would say if the answer turns out to be wrong in review. This is the paragraph that makes the interview answer land, because it shows a decision rather than a default.
- Bring the table and the projection into the chat. I will look for a denylist, for an unprojected history endpoint, and for a raw-document channel with a blank cell.
What this does not cover
This closes the state, checkpoints and time travel module. You can now address a run, read where it stopped, distinguish replaying it from forking it, and decide what any of it is allowed to show. The primitives map in the reference section is the one-page version to keep beside an unfamiliar graph while you use them.
What this lesson could not give you is the run’s interior. Checkpoints hold state between supersteps, not what happened inside a node: the prompt it sent, the latency, the tool that errored, the retry. That data lives in a tracing backend, and reading one is the traces-as-the-observability-substrate lesson in the next module.
Nothing here addressed the case where the schema is not yours to change because the graph is somebody else’s Python service and you are a TypeScript client on the other side of a wire. The field names differ, the packages differ, and the projection has to happen somewhere you may not own. That is the TypeScript-side-of-the-wire lesson, which opens the last module.
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 turns on one line of this page — that a snapshot’s values are the state channel values at that checkpoint — and on everything the page does not contain. Read it looking for a filter, a redaction hook, or a browser-safe projection. There is not one, and confirming that for yourself is worth more than taking this lesson’s word for it, because the assumption that some layer is handling this is exactly the assumption that ships.
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.