<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Priyansh Singhal</title>
    <description>The latest articles on DEV Community by Priyansh Singhal (@priyansh_singhal_5975e7d3).</description>
    <link>https://dev.to/priyansh_singhal_5975e7d3</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4124032%2Fed3aefc8-e71b-4c63-aaeb-68084cfc8d0b.jpg</url>
      <title>DEV Community: Priyansh Singhal</title>
      <link>https://dev.to/priyansh_singhal_5975e7d3</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/priyansh_singhal_5975e7d3"/>
    <language>en</language>
    <item>
      <title>I Split One Agent Into Three. The Plan Never Reached the Executor.</title>
      <dc:creator>Priyansh Singhal</dc:creator>
      <pubDate>Wed, 14 Oct 2026 07:05:35 +0000</pubDate>
      <link>https://dev.to/priyansh_singhal_5975e7d3/i-split-one-agent-into-three-the-plan-never-reached-the-executor-14k2</link>
      <guid>https://dev.to/priyansh_singhal_5975e7d3/i-split-one-agent-into-three-the-plan-never-reached-the-executor-14k2</guid>
      <description>&lt;p&gt;A LangGraph subgraph boundary dropped the plan, and the gain came from re-reading the source, not the plan.&lt;/p&gt;

&lt;p&gt;I gave a local 8B model a planning step before it did the real work, and modeled the handoff the way a LangGraph subgraph's own state schema actually behaves: a key the parent step produces but the child's schema never declares is simply gone on the other side. The plan never reached the next step intact: on every single task, the part that crossed that boundary was a 60-character fragment of what the planner actually wrote. I expected that to hurt. The three-step pipeline still beat the single-call baseline on accuracy, by a real margin. Those two facts together are the whole reason this is worth reading, because the second one does not mean what it looks like it means.&lt;/p&gt;

&lt;p&gt;I wrote this with AI assistance, and I fact-checked and edited every claim myself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proof block.&lt;/strong&gt; Ollama's &lt;code&gt;llama3.1:8b&lt;/code&gt;, running locally, zero API cost, temperature 0, fixed seed. 12 synthetic incident-report extraction tasks, one deterministic run per condition (a real, stated limit, not a hidden one). Condition A: one call per task. Condition B: a plain-Python three-step pipeline, planner then executor then reviewer, that models a LangGraph subgraph's state-schema boundary rather than running an actual LangGraph graph, 36 calls total. Every claim below traces to a public, re-runnable repository: &lt;a href="https://github.com/priyansh18/agent-handoff-loss-benchmark" rel="noopener noreferrer"&gt;github.com/priyansh18/agent-handoff-loss-benchmark&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Four things came out of this run worth stating before any explanation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The three-step pipeline scored 91.7% field accuracy against the single call's 83.3%, at 4.4x the tokens and 5.7x the wall-clock time.&lt;/li&gt;
&lt;li&gt;On 12 out of 12 tasks, the planner's own notes (368 to 584 characters each) were truncated to 60 characters before the executor ever saw them, a real state-schema mismatch, not a prompt bug.&lt;/li&gt;
&lt;li&gt;Three individual fields, across three different tasks, flipped from wrong to right. I logged the executor's intermediate answer separately from the reviewer's final one, specifically so I could check which step did the work instead of assuming: the executor alone, reading the full report plus whatever fragment of the hint survived, already reaches 88.9% (32/36) before the reviewer ever runs. The reviewer adds exactly one more field on top of that.&lt;/li&gt;
&lt;li&gt;None of the three fixes trace to the planner's hint carrying something useful across the boundary. Two happened with a hint that was pure boilerplate, no task-specific content at all. The third happened with a hint that did contain a genuinely useful word, by accident of where the planner's sentence break fell, not because the pipeline was designed to protect it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Multi-agent LangGraph handoff loss&lt;/strong&gt;, which is the specific failure this run reproduces in miniature, is the same shape of problem Anthropic names in its own &lt;a href="https://www.anthropic.com/engineering/multi-agent-research-system" rel="noopener noreferrer"&gt;multi-agent systems engineering writeup&lt;/a&gt;: agents typically use about 4x more tokens than a single chat turn, and a full multi-agent system about 15x, with part of that cost going toward what the piece calls a "game of telephone" when one agent's output gets summarized and re-summarized on its way to the next one. Anthropic's own fix is to have subagents write full output to a shared store instead of passing a compressed summary through conversation. I wanted a number for the failure mode before I believed the fix, not just a paragraph agreeing with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hypothesis
&lt;/h2&gt;

&lt;p&gt;Stated before running anything, so the result can actually fail it: &lt;em&gt;splitting a single extraction call into a planner-then-executor-then-reviewer pipeline, with the executor's state schema narrower than what the planner produces, will measurably lose information at the handoff, and that loss will show up as no better (or worse) accuracy than the single-call baseline, not as an improvement.&lt;/em&gt; The first half of that held. The second half was wrong, for a reason worth sitting with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method: the subgraph handoff
&lt;/h2&gt;

&lt;p&gt;Twelve short, fictional incident reports, written as test fixtures, each describing one bug: a Redis connection pool exhausted by a feature-flag rollout, a stale model artifact overwriting a cached file, a password-reset link truncated by a URL shortener, and nine more in the same shape. Each has a ground truth of required keywords for three fields: &lt;code&gt;root_cause&lt;/code&gt;, &lt;code&gt;affected_component&lt;/code&gt;, &lt;code&gt;fix_action&lt;/code&gt;. A field counts correct if the model's answer contains at least one of the required keywords for that field, a deliberately mechanical grader so the scoring is reproducible and not a matter of my own judgment call.&lt;/p&gt;

&lt;p&gt;Condition A asks &lt;code&gt;llama3.1:8b&lt;/code&gt; once per task for all three fields as JSON. Condition B runs three calls per task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The subgraph boundary, as code, not prose.
# The planner writes full notes; the executor only ever sees this:
&lt;/span&gt;&lt;span class="n"&gt;focus_hint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plan_notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;plan_notes&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line is the entire experiment's load-bearing detail, and it is plain Python, not a LangGraph &lt;code&gt;StateGraph&lt;/code&gt; call. I wrote it that way deliberately, to model something LangGraph's own subgraph docs describe without needing the framework's full runtime: a subgraph's state schema is what actually crosses the parent/child boundary, and a key the parent step produced but the child's schema never declared is simply absent on the other side. No exception, no warning. I made the schema narrow on purpose, the way a team under deadline pressure genuinely does, by wiring a field name before thinking hard about what actually needs to survive the handoff.&lt;/p&gt;

&lt;p&gt;The executor then extracts the three fields with that 60-character hint attached, and a reviewer sees the original report plus the executor's JSON and can correct it. Three calls, three chances to get something right, one shared report each time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results, before any explanation
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvfrzjtlybk549o0umqfl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvfrzjtlybk549o0umqfl.png" alt="Bar charts comparing field accuracy, total tokens, and wall-clock latency between the single-agent and three-step pipeline conditions, run against the same 12 tasks on a local Llama 3.1 8B model." width="799" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Single call vs. three-step pipeline, same 12 tasks, same model, same machine. Image by author, generated from the run's own summary.json.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The numbers, side by side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Field accuracy: 30/36 (83.3%) for the single call vs. 33/36 (91.7%) for the three-step pipeline.&lt;/li&gt;
&lt;li&gt;Tasks with all 3 fields correct: 7/12 vs. 9/12.&lt;/li&gt;
&lt;li&gt;LLM calls: 12 vs. 36.&lt;/li&gt;
&lt;li&gt;Total tokens: 1,659 vs. 7,323.&lt;/li&gt;
&lt;li&gt;Wall-clock time: 72.7s vs. 411.1s.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the handoff number, which the comparison above does not show: on 12 of 12 tasks, the full planner output ran 368 to 584 characters, and the &lt;code&gt;focus_hint&lt;/code&gt; that actually reached the executor was capped at 60. Here is what that looked like on one task, with the exact logged text, not a paraphrase:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6rkk2rs833p0gponjmsk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6rkk2rs833p0gponjmsk.png" alt="Diagram showing a planner's full investigation notes being cut to a 60-character fragment at the subgraph's state-schema boundary before reaching the executor, with the reviewer's corrected answer shown against the executor's wrong one, generated directly from this run's raw_results.jsonl." width="800" height="475"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Image by author, generated programmatically from the actual run's raw_results.jsonl.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Planner, in full, on the retry-storm task: "Here's a short investigation note: 'Review the system logs to identify the timing and duration of the downstream outage and its impact on the billing worker queue. Investigate the retry logic to determine why the system did not implement exponential backoff initially, and examine the configuration or code changes that were made to implement backoff. Additionally, assess whether the retry storm caused any data inconsistencies or other issues.'" What the executor received: "Here's a short investigation note: 'Review the system logs", 60 characters, cut before "retry storm" or "backoff" ever appear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure analysis: where the gain actually came from
&lt;/h2&gt;

&lt;p&gt;Three fields, across three different tasks, flipped from wrong to right between conditions. I logged the executor's intermediate answer separately from the reviewer's final one specifically so I could check which step did the work instead of assuming, and the three did not all resolve the same way.&lt;/p&gt;

&lt;p&gt;On &lt;code&gt;feature-flag-leak&lt;/code&gt;, the planner's hint was pure boilerplate: "Here's a short investigation note: &lt;strong&gt;Investigation Note&lt;/strong&gt;", 60 characters of preamble, zero task-specific content, the cleanest version of a useless hint in the whole run. The executor's own answer, before the reviewer ever touched it, still read "feature-flag targeting rule leak due to shared cache entry reuse", correct, despite the hint carrying nothing. It got there by reading the full original report, which every step in the pipeline receives in addition to whatever the planner's hint says.&lt;/p&gt;

&lt;p&gt;On &lt;code&gt;retry-storm&lt;/code&gt;, the hint was semi-generic ("Review the system logs"), not pure boilerplate, but not specific either. The executor's answer stayed wrong, same gap as the single call: "downstream outage," no mention of a retry storm. The reviewer step, given the full report a second time, corrected it to "downstream outage caused a retry storm" and passed. This is the one fix in the whole run that specifically needed the reviewer, not just the executor's own read.&lt;/p&gt;

&lt;p&gt;The third, on &lt;code&gt;redis-pool&lt;/code&gt;, is the one honest complication: the single call missed "redis" / "connection pool," but the executor fixed it, and this time the 60-character hint actually did contain "Review the Redis connection pool confi", the planner happened to lead with exactly the right words before the cutoff landed. I can't rule out the hint helping here, and saying it was "useless" for this task specifically would be overclaiming in the opposite direction. What I can say is that two of the three fixes happened with hints that contributed nothing, and the one case where the hint might have helped was a matter of where a sentence break happened to fall, not something the pipeline's schema was designed to guarantee.&lt;/p&gt;

&lt;p&gt;Put together: the planner step, measured honestly, earned nothing &lt;em&gt;reliable&lt;/em&gt; this run. Its one plausible contribution depended on accidental phrasing, not the architecture. The executor's own full-report read, independent of the hint, already gets to 88.9% (32/36), measured directly from the saved intermediate answers, not inferred. The reviewer adds exactly one more field on top of that, 33/36. If I had cut the planner and reviewer entirely and sent the report straight to one executor-shaped call, I would expect something close to that 88.9% figure, not the 83.3% single-call baseline, because the executor prompt itself asks for more structured output than the bare single-call prompt does. I did not run that exact ablation, so that specific comparison stays a reasoned expectation, not a measured result; everything else in this section is measured.&lt;/p&gt;

&lt;p&gt;There is a real theoretical reason to expect source re-reading to matter more than hand-off content here, not just my own anecdote. A &lt;a href="https://arxiv.org/abs/2604.02460" rel="noopener noreferrer"&gt;April 2026 Stanford preprint&lt;/a&gt;, "Single-Agent LLMs Outperform Multi-Agent Systems on Multi-Hop Reasoning Under Equal Thinking Token Budgets" by Dat Tran and Douwe Kiela, holds the reasoning-token budget constant across single- and multi-agent setups on three model families and finds single agents consistently match or beat multi-agent ones. Their explanation leans on the Data Processing Inequality: once one agent passes a processed version of its context to the next, information can only be lost or stay the same across that step, never gained. My &lt;code&gt;focus_hint&lt;/code&gt; truncation is that inequality made concrete and measurable, not an edge case. My own run adds one more data point in the same direction, with the mechanism visible in the logs rather than inferred: the win was real, 91.7% beats 83.3%, but it was bought mostly by steps re-reading the original source report, not by the architecture's planning step, which is the part most multi-agent write-ups spend the most words justifying.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a review step earns its cost, and when it doesn't
&lt;/h2&gt;

&lt;p&gt;The honest reading of this run is narrower than "multi-agent wins" or "multi-agent is a trap." Both would oversell a 12-task local run. What it does support: a step that looks at the original source material again rather than trusting an earlier step's summary of it can catch real mistakes, and that happened across three fields here, at the cost of 4.4x the tokens and 5.7x the latency. A planning pass that hands off through a schema too narrow for what it produced bought nothing reliable, and would have looked close to identical in the summary table if I had cut it entirely and gone straight from report to executor to reviewer.&lt;/p&gt;

&lt;p&gt;Before adding a planning step to any pipeline, the question worth asking is not "would more structure help," it's "what, specifically, needs to survive from this step to the next one, and does my state schema actually carry it." That is a concrete, checkable thing, not a vibe. Before adding a review step, the better question is whether the reviewer gets the original source material again or only the prior step's output, since my reviewer's advantage here came specifically from re-reading the report, not from critiquing the executor's JSON in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducibility and the honest gaps
&lt;/h2&gt;

&lt;p&gt;Full script, task set, raw per-task output, and the chart above, all generated directly from this run: &lt;a href="https://github.com/priyansh18/agent-handoff-loss-benchmark" rel="noopener noreferrer"&gt;github.com/priyansh18/agent-handoff-loss-benchmark&lt;/a&gt;. Everything ran locally against Ollama, zero API keys, reproducible on any machine with the same model pulled (exact numbers may still shift slightly with different hardware or Ollama versions, since wall-clock time and token counts aren't hardware-independent). One real limitation: one deterministic run per condition, not a repeated-trial distribution, so a single flipped field would move the accuracy numbers by one unit out of 36 (2.8 percentage points) either way; I'm stating the result as what happened in this run, not as a tight confidence interval. A second limitation: the keyword-based grader rewards completeness over precision, which partly explains why three other tasks stayed wrong in both conditions (detailed in the repo's &lt;code&gt;failure_examples/&lt;/code&gt;). I think it's fair for this task shape, but it's a judgment call, named here rather than left to make the numbers look cleaner than they are.&lt;/p&gt;

&lt;p&gt;If you've built a planner-executor-reviewer pipeline yourself: did the planning step ever get checked for what actually survives the handoff, or did it get trusted the same way I almost trusted mine?&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;LangGraph subgraphs and state-schema docs: &lt;a href="https://docs.langchain.com/oss/python/langgraph/use-subgraphs" rel="noopener noreferrer"&gt;docs.langchain.com/oss/python/langgraph/use-subgraphs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Anthropic, "How we built our multi-agent research system": &lt;a href="https://www.anthropic.com/engineering/multi-agent-research-system" rel="noopener noreferrer"&gt;anthropic.com/engineering/multi-agent-research-system&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tran and Kiela, "Single-Agent LLMs Outperform Multi-Agent Systems on Multi-Hop Reasoning Under Equal Thinking Token Budgets," April 2026: &lt;a href="https://arxiv.org/abs/2604.02460" rel="noopener noreferrer"&gt;arxiv.org/abs/2604.02460&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Full reproducibility package: &lt;a href="https://github.com/priyansh18/agent-handoff-loss-benchmark" rel="noopener noreferrer"&gt;github.com/priyansh18/agent-handoff-loss-benchmark&lt;/a&gt;, &lt;code&gt;tasks.py&lt;/code&gt; and &lt;code&gt;run_bench.py&lt;/code&gt; produced every number above, &lt;code&gt;raw_results.jsonl&lt;/code&gt; has the full per-task output including every truncated &lt;code&gt;focus_hint&lt;/code&gt; and the executor's intermediate answer, and &lt;code&gt;failure_examples/&lt;/code&gt; walks through each flipped and still-wrong task individually.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More from me: notes.priyanshsinghal.com has the roadmap these posts draw from, and I'm on LinkedIn at linkedin.com/in/priyanshsinghal.&lt;/p&gt;

</description>
      <category>langgraph</category>
      <category>ai</category>
      <category>python</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>What Your LangGraph Logs Miss When a Node Fails</title>
      <dc:creator>Priyansh Singhal</dc:creator>
      <pubDate>Wed, 07 Oct 2026 07:53:31 +0000</pubDate>
      <link>https://dev.to/priyansh_singhal_5975e7d3/what-your-langgraph-logs-miss-when-a-node-fails-3l97</link>
      <guid>https://dev.to/priyansh_singhal_5975e7d3/what-your-langgraph-logs-miss-when-a-node-fails-3l97</guid>
      <description>&lt;p&gt;&lt;em&gt;Two seeded failures in one small graph, and what each logging approach actually recorded.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When a LangGraph run dies, the log usually tells you the same thing twice: a node started, a node stopped, an exception was raised. It almost never tells you what the node was given. I wanted to know what a log has to contain before the failure happens, so I broke a three-node graph two different ways and checked what each run actually recorded.&lt;/p&gt;

&lt;p&gt;I wrote this with AI assistance, and I fact-checked and edited every claim myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof
&lt;/h2&gt;

&lt;p&gt;Every number in this piece comes from one run per scenario on LangGraph 1.2.12 with checkpoint 4.2.0, on Python 3.14.7. There is no LLM and no API key involved, so the API cost is zero. The three scenarios are a clean run, a transient timeout that a retry policy should absorb, and a fetch that returns an HTML error page where JSON is expected. Each run gets a fresh thread and an in-memory checkpointer. One run per scenario is enough to show the mechanism. It is not enough to estimate how often any of this happens in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The retry that succeeded left no trace in the task stream. Only a counter I put inside the node showed that fetch ran twice.&lt;/li&gt;
&lt;li&gt;The failing task did not produce an error or finish event in the stream. The exception came out of the stream call itself.&lt;/li&gt;
&lt;li&gt;The bad payload was visible in two places: the start event for parse, and the checkpoint saved just before the failure. My print statement threw both away.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Hypothesis
&lt;/h2&gt;

&lt;p&gt;If the checkpoint store records the state each node received, a failure can be explained from the store alone, without a print statement in the node. If it does not, the node-level logs are the only evidence you have. The runs below test that for the input, and separately check whether the stream reports retries at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method
&lt;/h2&gt;

&lt;p&gt;The graph has three nodes. Fetch can fail transiently, parse turns the fetched string into a dict, and write produces the result. The core of it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TypedDict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.graph&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;START&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.checkpoint.memory&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;InMemorySaver&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RetryPolicy&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: 7, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: [1, 2, 3]}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parsed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrote &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;parsed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;RetryPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry_on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;START&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpointer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;InMemorySaver&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;configurable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thread_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tasks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transient scenario makes fetch's first call raise a &lt;code&gt;TimeoutError&lt;/code&gt;. The bad-input scenario keeps the same code but makes fetch return &lt;code&gt;&amp;lt;html&amp;gt;error page&amp;lt;/html&amp;gt;&lt;/code&gt;. The graph runs the same way in both. Only fetch's body changes.&lt;/p&gt;

&lt;p&gt;The snippet compiles with an in-memory checkpointer so the saved steps can be read after the run. The task stream itself does not need one. I ran the same three-node graph with no checkpointer and it still emitted a start and a finish for every node.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each run recorded
&lt;/h2&gt;

&lt;p&gt;The clean run saved five checkpoints: the input, one after each of the three nodes, and the end. The task stream showed a start and a finish for fetch, parse and write. Nothing surprising happened, which is the point of a baseline.&lt;/p&gt;

&lt;p&gt;The transient run called fetch twice. The task stream still showed one start and one finish for fetch, with no mention of the first attempt. The run completed with the same five checkpoints as the clean run. From the stream, the retry looks like it never happened. The only record of it was the counter I put inside the node.&lt;/p&gt;

&lt;p&gt;The bad-input run saved three checkpoints and then raised &lt;code&gt;JSONDecodeError&lt;/code&gt; inside parse. The most recent checkpoint is at step 1, and its next node is parse. Its state holds the raw &lt;code&gt;&amp;lt;html&amp;gt;error page&amp;lt;/html&amp;gt;&lt;/code&gt; string, which is the exact input that broke parse. On the stream, parse's start event also carried that input. Then the exception came out of the stream call. I did not get a finished event for parse, and no event carried the error.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Checkpoint timeline diagram: see the original post for the visual.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure analysis
&lt;/h2&gt;

&lt;p&gt;The failure reached my code as an exception from the stream call, not as an event. A logger that only records task finishes or error events would miss it. The input was not missing from the stream, though. The start event for parse carried it, and so did the checkpoint. Both are only useful if your code keeps them, which a print statement that records just the task name does not.&lt;/p&gt;

&lt;p&gt;The stream also does not report the retry, at least in tasks mode. A retry that succeeds is invisible to that view. LangGraph's own retry code does write a line through its logger at INFO level (&lt;code&gt;langgraph.pregel._retry&lt;/code&gt;, "Retrying task fetch ... (attempt 1)"), so the record exists outside the stream. If you do not enable that logger, the node has to count the attempts itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to log before it fails
&lt;/h2&gt;

&lt;p&gt;Here is what I would log on every node, based on what the two failures showed. Each item maps to something my print-based approach threw away or could not see.&lt;/p&gt;

&lt;p&gt;First, capture retry attempts. Either count them inside the node, or enable the retry logger at INFO so each retry is recorded with its exception. Second, record the input each node received, or at least its size and a hash. The checkpoint held the HTML here, but a checkpoint only exists if you persisted one. Third, put the thread ID and the checkpoint step on every log line, so a log entry can be joined to the saved state. Fourth, log the exception together with the payload that caused it, at the exact boundary where parsing happens.&lt;/p&gt;

&lt;p&gt;There is a cost to the second and fourth items. Checkpoints store the full state, and logs that include raw payloads can leak whatever the payload contains. Log size and hash by default, and keep full payloads only for nodes that failed, with redaction before anything leaves the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is enough, and when it is not
&lt;/h2&gt;

&lt;p&gt;The checkpoint store is enough when you have one and you can read it after the run. Without a checkpointer, the task stream still reports each node's input, but there are no saved steps to read afterwards, so the record lasts only as long as your logs. Retry attempts are the one thing neither the stream nor the checkpoint shows, so they need the node counter or the retry logger either way. If your payloads are large or sensitive, store a hash and a size, and treat the checkpoint itself as data that needs the same retention rules as your logs.&lt;/p&gt;

&lt;p&gt;Start with the bad input, because that is what the store can explain. The retry count is the piece you have to add yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;LangGraph streaming docs: docs.langchain.com/oss/python/langgraph/streaming&lt;/li&gt;
&lt;li&gt;LangGraph persistence docs: docs.langchain.com/oss/python/langgraph/persistence&lt;/li&gt;
&lt;li&gt;I have not published a repository for this experiment yet. The graph above is the full core of it, and the two failure variants change only the body of fetch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The question I'd like you to answer in the comments: on a parse node, would you log the raw payload, its hash, or nothing, and what would you do with that log once the run succeeded?&lt;/p&gt;

&lt;p&gt;More from me: notes.priyanshsinghal.com has the roadmap these posts draw from, and I'm on LinkedIn at linkedin.com/in/priyanshsinghal.&lt;/p&gt;

</description>
      <category>langgraph</category>
      <category>python</category>
      <category>ai</category>
      <category>observability</category>
    </item>
    <item>
      <title>Your LangGraph Node Runs Twice After interrupt(). That's by Design.</title>
      <dc:creator>Priyansh Singhal</dc:creator>
      <pubDate>Wed, 30 Sep 2026 08:59:08 +0000</pubDate>
      <link>https://dev.to/priyansh_singhal_5975e7d3/your-langgraph-node-runs-twice-after-interrupt-thats-by-design-796</link>
      <guid>https://dev.to/priyansh_singhal_5975e7d3/your-langgraph-node-runs-twice-after-interrupt-thats-by-design-796</guid>
      <description>&lt;p&gt;&lt;em&gt;Checkpoints are save points at super-step boundaries. Resume, durability and subgraph state all follow from that.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Put a &lt;code&gt;print&lt;/code&gt; on the first line of a LangGraph node, call &lt;code&gt;interrupt()&lt;/code&gt; a few lines later, resume the graph with a human's answer, and the print fires twice. The first time you see that you assume you wired something wrong. You didn't. LangGraph does not resume a paused node from the line it stopped on. It goes back to the last saved checkpoint and runs the whole node again from the top, and this time &lt;code&gt;interrupt()&lt;/code&gt; returns the answer instead of pausing.&lt;/p&gt;

&lt;p&gt;Once that clicks, three things tutorials teach as separate features (human-in-the-loop, crash recovery, time travel) turn out to be one mechanism seen from three angles. A checkpoint is a save point. Everything else is what you can do with a save point. This is the explanation I wanted while working through human-in-the-loop in my own agent notes, so here it is, with the 20-line script that reproduces the double print. Every behaviour below was run on langgraph 1.2.12, not just read in the docs; the one place they disagree is called out.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I wrote this with AI assistance, and I fact-checked and edited every claim myself.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What a checkpoint actually is
&lt;/h2&gt;

&lt;p&gt;LangGraph runs a graph in rounds it calls super-steps. In one super-step, every node that is scheduled runs (in parallel if there are several), their state updates are merged, and the runtime works out which nodes fire next. A plain &lt;code&gt;START -&amp;gt; a -&amp;gt; b -&amp;gt; END&lt;/code&gt; graph is three super-steps: one for the input, one for &lt;code&gt;a&lt;/code&gt;, one for &lt;code&gt;b&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The docs put it in one line: LangGraph creates a checkpoint at each super-step boundary. Attach a checkpointer, run that three-node graph on a thread, and &lt;code&gt;get_state_history&lt;/code&gt; hands back four snapshots: an empty one before the input landed, then one after each step. Each snapshot holds the full state values at that moment, the nodes due to run next, a step number, and a link to its parent snapshot. That is the entire save file.&lt;/p&gt;

&lt;p&gt;Two details matter for everything that follows. First, checkpoints are keyed by &lt;code&gt;thread_id&lt;/code&gt;, passed in the config on every call. A new thread starts with empty state and never sees another thread's history. Second, the docs are precise about scope: checkpointers are thread-scoped, short-term memory (conversation continuity, human-in-the-loop, time travel, fault tolerance), and a separate store handles cross-thread, long-term memory (user preferences, facts). A checkpointer remembers a thread. It does not remember a user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the node runs twice
&lt;/h2&gt;

&lt;p&gt;Here is the smallest reproduction. No model, no API key, just a node that asks a human before it commits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TypedDict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.graph&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;START&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.checkpoint.memory&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;InMemorySaver&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;interrupt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Command&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TypedDict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;approve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approve node entered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# runs before the pause
&lt;/span&gt;    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;interrupt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ship it?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c1"&gt;# pause here, wait for a human
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resumed with &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;approve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;START&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpointer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;InMemorySaver&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;configurable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thread_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# pauses at interrupt
&lt;/span&gt;&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# human answered
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On langgraph 1.2.12 the output is three lines: &lt;code&gt;approve node entered&lt;/code&gt;, &lt;code&gt;approve node entered&lt;/code&gt;, &lt;code&gt;resumed with 'yes'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Walk through what happened. The first &lt;code&gt;invoke&lt;/code&gt; enters the node, prints, reaches &lt;code&gt;interrupt("Ship it?")&lt;/code&gt;, and stops. The runtime writes a checkpoint for the thread, and the return value of &lt;code&gt;invoke&lt;/code&gt; carries the question under a key called &lt;code&gt;__interrupt__&lt;/code&gt;. Nothing after the interrupt line ran, so &lt;code&gt;decision&lt;/code&gt; in state is still the empty string you seeded.&lt;/p&gt;

&lt;p&gt;The second &lt;code&gt;invoke&lt;/code&gt; passes &lt;code&gt;Command(resume="yes")&lt;/code&gt; on the same thread. This is the part the docs state plainly and most tutorials skip: the runtime restarts the entire node from the beginning. It does not resume from the exact line where &lt;code&gt;interrupt&lt;/code&gt; was called. So the print fires again, execution reaches &lt;code&gt;interrupt()&lt;/code&gt; again, and this time the call does not pause. The value you passed in &lt;code&gt;Command(resume=...)&lt;/code&gt; becomes its return value. The node carries on to the second print and returns.&lt;/p&gt;

&lt;p&gt;My read on why it works this way: the alternative is freezing a Python call stack mid-function, and nothing serializes that safely across a restart, a different worker, or a deploy. Saving state at a boundary and replaying is what lets a pause be resumed by something other than the exact process that paused. The docs describe the model in one sentence: execution returns to a checkpoint boundary, and the workflow replays forward until it reaches the pause again.&lt;/p&gt;

&lt;p&gt;Three consequences follow, and each one is easy to trip over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anything before &lt;code&gt;interrupt()&lt;/code&gt; must be safe to run twice.&lt;/strong&gt; The docs say side effects called before &lt;code&gt;interrupt&lt;/code&gt; must be idempotent, and their own example is the database row: inserting one before the pause inserts it again on resume. Put the write after the interrupt, or make it an upsert, or use an idempotency key. Do not put the email send on line one and the approval on line three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No checkpointer, no resume.&lt;/strong&gt; Run the snippet above without &lt;code&gt;checkpointer=InMemorySaver()&lt;/code&gt; and the first call still returns &lt;code&gt;__interrupt__&lt;/code&gt;, which is misleading, because there is nowhere to resume from. The second call raises &lt;code&gt;RuntimeError: Cannot use Command(resume=...) without checkpointer&lt;/code&gt;. The docs list a checkpointer as requirement number one for &lt;code&gt;interrupt&lt;/code&gt;, and the error message is the runtime agreeing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Order matters if a node interrupts more than once.&lt;/strong&gt; Resume values are matched to interrupt calls strictly by index, in the order the calls happen during the replay. A node with two interrupts takes two resumes, and the first resume value lands in the first call. Add a conditional interrupt before an existing one and the wrong answer lands in the wrong call. Keep interrupts in a node few and their order fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Durability: what survives a crash
&lt;/h2&gt;

&lt;p&gt;If checkpoints are save points, the next question is how often the game saves. LangGraph exposes that as a &lt;code&gt;durability&lt;/code&gt; argument on &lt;code&gt;invoke&lt;/code&gt; and &lt;code&gt;stream&lt;/code&gt;, with three modes. Leave it unset and 1.2.12 uses &lt;code&gt;"async"&lt;/code&gt;; the default is in &lt;code&gt;langgraph/pregel/main.py&lt;/code&gt;, the docs page names none.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;"exit"&lt;/code&gt; persists only when the run exits, whether by finishing, erroring, or hitting an interrupt. Fastest, and intermediate state is not saved, so a process crash mid-run loses everything since the start. &lt;code&gt;"async"&lt;/code&gt; writes each checkpoint while the next step is already executing, which the docs describe as good performance and durability with a small risk of a missed checkpoint if the process dies at the wrong moment. &lt;code&gt;"sync"&lt;/code&gt; writes every checkpoint before the next step starts, highest durability, some overhead.&lt;/p&gt;

&lt;p&gt;You can see the difference without a crash. The three-node graph from earlier leaves four snapshots in history under &lt;code&gt;"async"&lt;/code&gt; or &lt;code&gt;"sync"&lt;/code&gt;, and exactly one under &lt;code&gt;"exit"&lt;/code&gt;. That one is the finished state. Time travel to the point before &lt;code&gt;b&lt;/code&gt; is impossible on that thread, because the save point was never written.&lt;/p&gt;

&lt;p&gt;My view: &lt;code&gt;"exit"&lt;/code&gt; is right for a cheap graph where a retry from scratch costs nothing, and wrong the moment a node does anything you would not want repeated. And no mode helps if the checkpointer itself is &lt;code&gt;InMemorySaver&lt;/code&gt;. It keeps checkpoints in RAM, and the docs say it plainly: when the process restarts, all checkpoints are lost. Most examples, the docs' own included, use it because it needs no setup. Production needs &lt;code&gt;PostgresSaver&lt;/code&gt; or at least &lt;code&gt;SqliteSaver&lt;/code&gt;, or your carefully durable checkpoints evaporate on the next deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Subgraphs and time travel are the same save point
&lt;/h2&gt;

&lt;p&gt;The two features that look most separate are the ones that fall out of the model most directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subgraphs.&lt;/strong&gt; Compile a subgraph without a checkpointer and add it as a node in a parent that has one, and the subgraph inherits the parent's checkpointer. An &lt;code&gt;interrupt()&lt;/code&gt; inside the subgraph surfaces at the parent's &lt;code&gt;invoke&lt;/code&gt; exactly like a top-level one, and &lt;code&gt;Command(resume=...)&lt;/code&gt; on the parent resumes it. Its checkpoints live in the same store under a namespace of the form &lt;code&gt;child_node_name:uuid&lt;/code&gt;, nested subgraphs joined with &lt;code&gt;|&lt;/code&gt;. You can read them with &lt;code&gt;get_state(config, subgraphs=True)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Where the save points sit decides what replays. From the parent's history the whole subgraph is one step: &lt;code&gt;START -&amp;gt; child -&amp;gt; END&lt;/code&gt; leaves three snapshots however many nodes the child has. The child's own save points live under its namespace, so when a two-node subgraph pauses in its second node, resume re-runs only that node; the first one's result was already saved. &lt;code&gt;checkpointer=True&lt;/code&gt; drops the uuid from the namespace and, per the docs, makes the subgraph per-thread: its state carries across calls on the same thread. &lt;code&gt;checkpointer=False&lt;/code&gt; saves nothing inside. The docs say no interrupts or durable execution. On 1.2.12 it was one step short of that, and it is the place the docs and the run disagree: the interrupt still surfaced, the resume still completed, but both nodes ran again, the first one included, because there was no save point inside to replay from. Same rule, one level up: everything in a stateless subgraph before the pause must be safe to run twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time travel.&lt;/strong&gt; This is loading an older save. Pull the history, pick the snapshot whose &lt;code&gt;next&lt;/code&gt; is the node you want to re-run, call &lt;code&gt;update_state&lt;/code&gt; on that snapshot's config with new values, then &lt;code&gt;invoke(None, fork_config)&lt;/code&gt;. On the three-node graph, forking the snapshot before &lt;code&gt;b&lt;/code&gt; with &lt;code&gt;n=100&lt;/code&gt; returns &lt;code&gt;n=1000&lt;/code&gt;, because &lt;code&gt;b&lt;/code&gt; multiplies by ten and never knew the original run existed. The fork is a new branch on the same thread; the original checkpoints stay in history, which is what makes this safe for debugging a bad agent turn.&lt;/p&gt;

&lt;p&gt;Seen this way, human-in-the-loop, fault tolerance, subgraph state and time travel are one feature with four names. The runtime writes a snapshot at every boundary, and resuming from any snapshot replays forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to actually use this
&lt;/h2&gt;

&lt;p&gt;A short checklist, in the order the mistakes usually happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decide what a &lt;code&gt;thread_id&lt;/code&gt; means in your app before writing a node. One per conversation is the usual answer. One per user is a store's job, not a checkpointer's.&lt;/li&gt;
&lt;li&gt;In any node that calls &lt;code&gt;interrupt()&lt;/code&gt;, move side effects below it or make them idempotent. Read the node top to bottom and ask what happens if this runs twice.&lt;/li&gt;
&lt;li&gt;Pick &lt;code&gt;durability&lt;/code&gt; on purpose per graph instead of taking the &lt;code&gt;"async"&lt;/code&gt; default. Long, expensive, or side-effecting runs want &lt;code&gt;"sync"&lt;/code&gt;; cheap replayable ones can take &lt;code&gt;"exit"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Swap &lt;code&gt;InMemorySaver&lt;/code&gt; for a persistent saver before the first deploy, not after the first lost approval.&lt;/li&gt;
&lt;li&gt;For each subgraph, choose: inherit (default), carry state across calls on the thread (&lt;code&gt;checkpointer=True&lt;/code&gt;), or stateless (&lt;code&gt;checkpointer=False&lt;/code&gt;). Stateless means the whole subgraph replays on resume, so rule 2 applies to every node in it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you are working through this stage yourself and want to talk through any of it live, I keep a slot open on &lt;a href="https://topmate.io/priyansh_singhal10" rel="noopener noreferrer"&gt;Topmate&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;More from me: the notes this came out of are at &lt;a href="https://notes.priyanshsinghal.com/docs/agentic-course/23-human-in-the-loop-langgraph" rel="noopener noreferrer"&gt;notes.priyanshsinghal.com&lt;/a&gt;, part of the full roadmap at &lt;a href="https://notes.priyanshsinghal.com" rel="noopener noreferrer"&gt;notes.priyanshsinghal.com&lt;/a&gt;, and I post shorter breakdowns of what I'm learning on &lt;a href="https://linkedin.com/in/priyanshsinghal" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>langgraph</category>
      <category>python</category>
      <category>ai</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>The LangChain You Learned in 2024 Doesn't Exist Anymore</title>
      <dc:creator>Priyansh Singhal</dc:creator>
      <pubDate>Wed, 23 Sep 2026 09:23:46 +0000</pubDate>
      <link>https://dev.to/priyansh_singhal_5975e7d3/the-langchain-you-learned-in-2024-doesnt-exist-anymore-2p95</link>
      <guid>https://dev.to/priyansh_singhal_5975e7d3/the-langchain-you-learned-in-2024-doesnt-exist-anymore-2p95</guid>
      <description>&lt;p&gt;&lt;em&gt;LLMChain and its siblings moved to a legacy package. The new core is one function, create_agent, plus middleware.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Open a LangChain tutorial from 2024, follow it against a fresh install, and the imports fail before you write any logic. &lt;code&gt;LLMChain&lt;/code&gt; is not in the main package anymore. Neither is &lt;code&gt;initialize_agent&lt;/code&gt;, nor most of the chain zoo that tutorials spent two years teaching. That is not deprecation drift. It was a deliberate rebuild: in October 2025 LangChain shipped 1.0 and cut the framework down to what its team calls essential abstractions, moving everything else into a separate legacy package.&lt;/p&gt;

&lt;p&gt;I hit this wall while working through the agentic AI stage of my own learning roadmap, with a browser full of tutorials describing a library that no longer matches what pip installs. This article is the map I wanted that week: what actually changed, what the new core looks like, and how to read older material without getting burned.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 1.0 actually changed
&lt;/h2&gt;

&lt;p&gt;LangChain 1.0 went generally available on October 22, 2025, alongside LangGraph 1.0. Two changes matter more than the version number.&lt;/p&gt;

&lt;p&gt;First, the scope cut. The main &lt;code&gt;langchain&lt;/code&gt; package no longer tries to be a warehouse of every chain pattern from the last three years. It focuses on agents, models, messages, and tools. Everything legacy, including &lt;code&gt;LLMChain&lt;/code&gt; and the old pre-built chains, moved to a package literally named &lt;code&gt;langchain-classic&lt;/code&gt; (&lt;code&gt;@langchain/classic&lt;/code&gt; in JavaScript). Old code did not stop existing, it changed address. In current code the old import reads &lt;code&gt;from langchain_classic.chains import LLMChain&lt;/code&gt;, and that rename is doing honest work: the framework is telling you this is the museum wing.&lt;/p&gt;

&lt;p&gt;Second, a stability promise. 1.0 commits to no breaking changes until 2.0. That sounds boring, but for a library famous for breaking tutorials every few months it is arguably the biggest feature in the release. What you learn on the 1.x core should keep compiling for a while.&lt;/p&gt;

&lt;p&gt;The reason for the cut is visible in what survived. Chains were LangChain's answer to a 2023 question: how do I glue prompts together? The 2026 question is different, how do I run a model in a loop with tools, safely. Agents won, and the package now says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  The new core is one function
&lt;/h2&gt;

&lt;p&gt;Here is a complete working agent on the current API, straight from the quickstart shape in the docs. One prerequisite the snippet itself won't remind you of: export the API key for whichever provider you're calling first (&lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;, &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;, and so on), or the agent has nothing to authenticate with.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.agents&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_agent&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get weather for a given city.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;It&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s always sunny in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anthropic:claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a helpful assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the weather in San Francisco?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;content_blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;create_agent&lt;/code&gt; is the whole story. You hand it a model, a list of plain Python functions as tools, and a system prompt. The model argument is a provider-prefixed string (&lt;code&gt;"anthropic:claude-sonnet-4-6"&lt;/code&gt;, &lt;code&gt;"openai:gpt-5.5"&lt;/code&gt;, or any &lt;code&gt;"ollama:"&lt;/code&gt; model tag), which makes trying your agent against a local model a one-line experiment. At runtime it loops: the model reads the messages and either emits tool calls or a final answer. Tool calls get executed, their results are appended to the message list as tool messages, and the model runs again. A response with no tool calls ends the loop.&lt;/p&gt;

&lt;p&gt;Two details worth knowing before you build anything real. This function replaces &lt;code&gt;create_react_agent&lt;/code&gt; from LangGraph, which is now deprecated, so treat any tutorial teaching that name as pre-1.0. And it runs on the LangGraph runtime underneath, which matters later. Note also &lt;code&gt;content_blocks&lt;/code&gt; on the final message: 1.0's standardized way to read reasoning traces, citations, and tool calls in one consistent shape across providers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Middleware is the actual headline
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;create_agent&lt;/code&gt; got the announcement-post attention. The part I think matters more day to day is middleware, which the docs describe as hooks that let you customize behavior at every step of the agent loop.&lt;/p&gt;

&lt;p&gt;Before 1.0, controlling the middle of an agent's execution meant callback handlers, subclassing, or forking a prebuilt graph. Most tutorials just skipped the problem. Now there are three obvious seams, named for exactly where they cut in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@before_model&lt;/code&gt; runs before each model call. This is where you trim context or enforce a call budget. The docs' own example counts model calls in state and jumps to the end past a limit, which is a real runaway-agent guard in about ten lines.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@after_model&lt;/code&gt; runs after each model response, before anything acts on it. Output validation and redaction live here.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@wrap_tool_call&lt;/code&gt; wraps each tool execution like an HTTP middleware wraps a request. You get the request, you own calling the handler, you can log, retry, or replace the result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are also built-ins for the three things everyone eventually hand-rolls: human-in-the-loop approval for dangerous tools, PII redaction, and conversation summarization. The human-in-the-loop one is configured per tool, so &lt;code&gt;HumanInTheLoopMiddleware(interrupt_on={"send_email": True})&lt;/code&gt; pauses the loop for approval before that one tool fires while every read-only tool runs untouched. One catch before you demo it: the pause is a LangGraph interrupt underneath, so the middleware line alone does not give you a working approval flow. You also need a checkpointer attached to the agent (&lt;code&gt;InMemorySaver&lt;/code&gt; for local work, something persistent in production) and a thread ID passed with every invoke, and resuming means calling the agent again with a &lt;code&gt;Command&lt;/code&gt; carrying the decision. Three extra lines, but the docs only make this clear once you are deep in the human-in-the-loop page, and every demo that skips them dies at the first pause. The summarization one is configured with a token threshold and a keep-last-N-messages count, and it quietly solves the failure mode where a long-running agent drowns in its own history. That used to be a weekend of custom code. My honest read: if you evaluate LangChain 1.0 by &lt;code&gt;create_agent&lt;/code&gt; alone it looks like a nicer constructor, and the middleware system is the actual argument for using the framework instead of writing the loop yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to read old tutorials without getting burned
&lt;/h2&gt;

&lt;p&gt;A practical triage, since most LangChain material online is still pre-1.0:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Date the code, not the post.&lt;/strong&gt; Anything showing &lt;code&gt;LLMChain&lt;/code&gt;, &lt;code&gt;initialize_agent&lt;/code&gt;, or &lt;code&gt;create_react_agent&lt;/code&gt; predates October 2025 in spirit, whatever its publish date says. The concepts often still transfer. The imports will not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat &lt;code&gt;langchain-classic&lt;/code&gt; as an escape hatch, not a destination.&lt;/strong&gt; It exists so working systems keep running, and if you maintain one, updating an import path beats a rewrite. I would not point new code at it. You would be learning the museum wing while the actual exhibit is one function with clean seams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expect some patterns to have moved into the loop.&lt;/strong&gt; Structured output is the clearest example: it now happens inside the main agent loop rather than as a separate follow-up LLM call, which is one less round trip and one less place to disagree with your own schema. If a tutorial bolts an output parser onto the end of a chain, that is the old world showing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where LangChain ends and LangGraph begins
&lt;/h2&gt;

&lt;p&gt;The framework's own framing is that you start with LangChain's high-level API and drop down to LangGraph when you need more control. In practice the boundary is shape: one model looping over tools is &lt;code&gt;create_agent&lt;/code&gt; territory, even with heavy middleware. The moment you need multiple agents, branching workflows, or explicit state machines, you are describing a graph, and pretending otherwise just means building a graph badly out of middleware.&lt;/p&gt;

&lt;p&gt;The two worlds compose rather than compete. An agent built with &lt;code&gt;create_agent&lt;/code&gt; can be added as a single node inside a LangGraph &lt;code&gt;StateGraph&lt;/code&gt;, middleware and all, so graduating to a graph does not mean rewriting the agent you already have. You wire it in next to your routing and classification nodes and it keeps behaving exactly as it did standalone.&lt;/p&gt;

&lt;p&gt;Which suggests a learning order I wish more roadmaps made explicit. Learn the loop first, then middleware, and only then LangGraph. Every graph node you will ever write is easier to reason about once you can explain what one agent loop does, because underneath, that is what is running.&lt;/p&gt;

&lt;p&gt;If you are working through this stage yourself and want to talk through any of it live, I keep a slot open on &lt;a href="https://topmate.io/priyansh_singhal10" rel="noopener noreferrer"&gt;Topmate&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;More from me: the full roadmap this article is part of is at &lt;a href="https://notes.priyanshsinghal.com" rel="noopener noreferrer"&gt;notes.priyanshsinghal.com&lt;/a&gt;, and I post shorter breakdowns of what I'm learning on &lt;a href="https://linkedin.com/in/priyanshsinghal" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>langchain</category>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
