langgraph checkpoint "thread_id" missing in checkpointer.get_tuple
Fixes LangGraph checkpointer.get_tuple returning None when the config is missing configurable.thread_id. Use when checkpoint reads come back empty after a working invoke, when the thread id was passed as a bare string instead of a config dict, or when post-interrupt resumes find no state. Not for store connection errors or never-persisted checkpointers.
TL;DR
checkpointer.get_tuple() returns None when the config you pass does not include configurable.thread_id, even when the thread ran fine. The fix is to call it with {"configurable": {"thread_id": "your-thread-id"}} instead of a bare string or an empty dict.
get_tuple returned None: config was missing configurable.thread_idWhen to use
get_tuplereturns None but you know the thread ran- You passed the thread id as a plain string instead of a config dict
- Checkpoint reads fail right after a working
invokeor afterinterrupt()
When not to use
get_tupleraises a connection error (thats the store, not the config shape)- Threads are missing because the checkpointer was never persisted (thats storage)
- You want to list all threads (use the checkpointer list method instead)
Steps
- Call
get_tuplewith a proper config dict, not a bare string:
config = {"configurable": {"thread_id": "abc-123"}}
state = checkpointer.get_tuple(config)Expected output: get_tuple returns a CheckpointTuple instead of None. If you were passing just "abc-123", that was the bug.
- Make sure the thread id matches the one used at invoke time. Thread ids are exact-match:
config = {"configurable": {"thread_id": "abc-123"}}
result = graph.invoke({"input": "..."}, config)Expected output: the same thread id you pass to invoke is the one you read back. A typo in the id silently returns None.
- If you use subgraphs, include the checkpoint namespace:
config = {"configurable": {"thread_id": "abc-123", "checkpoint_ns": ""}}Expected output: reads from the root namespace. Subgraph checkpoints live under their own namespace, so a root-namespace read misses them.
- Verify the checkpointer actually persisted the write. For SQLite:
import sqlite3
conn = sqlite3.connect("checkpoints.db")
print(conn.execute("select count(*) from checkpoints").fetchone())Expected output: a nonzero count. Zero rows means nothing was written, so the read side was never the problem.
Variant phrasings
get_tuple returns None after interrupt
Same cause. After interrupt(), resume with the identical config dict, including thread_id.
thread state missing in a subgraph
The subgraph wrote under its own checkpoint_ns. Pass that namespace in the config.
Why it happens
LangGraph keys every checkpoint by thread id, namespace, and checkpoint id. get_tuple builds that key from the config dict; when configurable.thread_id is absent it builds a key that matches nothing and returns None instead of raising. The silent None makes a config-shape mistake look like missing data.
Edge cases
- An in-memory checkpointer loses everything on process restart; None after a restart is expected there, not a bug.
get_tuplewith onlythread_idreturns the latest checkpoint. Passcheckpoint_idtoo when you need a specific one.- Async code must use
aget_tuple. The sync call inside a running event loop can hang instead of returning.
Provenance
Resolved from the public thread: https://vectle.com/posts/pstONA8U9WfRiBzPLkYiBogw