LangGraph Studio is a local visual IDE that lets you connect a running LangGraph agent, step through its graph execution node by node, inspect live…
Install the LangGraph CLI (v0.4.31+), write a minimal langgraph.json manifest, and confirm the Studio UI loads at localhost:8123. You leave this module with a running server pointed at a placeholder graph.
How to install LangGraph CLI, write a langgraph.json manifest, and launch the Studio dev server at localhost:8123.
Why this matters: This is the foundation for every other Studio feature — you can't inspect, debug, or replay your agent graph until the server is running and pointed at your code.
Decision this forces: Docker-based server vs. in-process dev mode — which fits your local setup?
Your agent just misbehaved and you have no idea which made the wrong decision. How do you see inside the graph without littering your code with print statements?
is a browser-based visual debugger for LangGraph agent graphs. It connects to a local dev server, renders your graph topology, and streams each step of the in real time.
The (v0.4.31+) is the tool that starts that server. It reads a manifest you write, finds your graph entry point, and serves the Studio UI at localhost:8123.
By the end of this module you'll have that server running against a placeholder graph — which is the foundation every later module builds on.
# Looks fine — but installs the wrong package pip install langgraph # Then you try to start Studio: langgraph dev # zsh: command not found: langgraph
The most common first mistake is installing langgraph (the runtime library) instead of langgraph-cli (the tool that provides the langgraph command). They are separate packages.
The shell reports command not found: langgraph because langgraph (the runtime) ships no CLI entry point. The fix is pip install "langgraph-cli>=0.4.31" — that package installs the langgraph binary.
# 1. Install the CLI (v0.4.31+ required for Studio) pip install "langgraph-cli>=0.4.31" # 2. Confirm the version langgraph --version # langgraph-cli 0.4.31 # 3. Create a project folder and enter it mkdir my-agent && cd my-agent
Install with a version pin so you get the pre-built container image support introduced in 0.4.31. The version check confirms the binary is on your PATH before you write any config.
langgraph-cli 0.4.31 (or higher). If you see command not found, the install failed or your venv isn't activated.
{
"dependencies": ["."],
"graphs": {
"my_graph": "./graph.py:graph"
},
"env": ".env"
}The manifest is the only config file Studio reads. "dependencies": ["."] tells the CLI to install the current directory as a package. "graphs" maps a name to a Python module path and the variable name of your compiled graph object.
"<file>:<variable>" — the variable must be a compiled LangGraph graph object at module load time, not a factory function.The server starts but then throws an AttributeError: module 'graph' has no attribute 'graph' (or similar import error). Studio shows a blank topology panel. Fix: update the manifest to "./graph.py:agent".
# In-process dev mode (no Docker needed) langgraph dev # Expected output: # Starting LangGraph API server... # Ready! Server running at http://localhost:8123 # Studio UI: https://smith.langchain.com/studio/?baseUrl=http://localhost:8123 # Docker-based (swap the command if you prefer isolation) langgraph up
Run langgraph dev for in-process mode or langgraph up for Docker. Both serve the API at port 8123; Studio opens via the printed URL.
Open the printed Studio URL in your browser. You should see the graph topology panel with a node named after your graph variable. If the panel is blank, the manifest path is wrong — re-check the "graphs" entry.
The "graphs" path in langgraph.json points to a file or variable that doesn't exist, so the server loaded but registered no graphs. Check the file path and variable name match exactly — Python is case-sensitive.
| Option | Isolation from host Python env | Cold-start speed | When to choose | Cost | Complexity |
|---|---|---|---|---|---|
| Docker-based server | Full — runs in its own container | Slow on first pull; fast after cache | When you want a clean, reproducible environment that matches production deployment or when your graph has native/binary dependencies. | Slightly more RAM; image layers cached after first run. | Higher — Docker Desktop must be running; first pull can take minutes. |
| In-process dev mode | None — uses your active venv | Near-instant; no image pull | When you want the fastest iteration loop on a local machine and your graph has no native binary deps. | Shares your host Python environment; dependency conflicts are possible. | Low — just pip-install and run; no Docker needed. |
OSError: [Errno 98] Address already in use. Run lsof -i :8123 to find the process. Kill it or pass --port 8124 to the CLI.env file causes silent auth failures. If "env": ".env" is set but missing, the server starts fine. Any node reading OPENAI_API_KEY gets None and fails mid-run with a cryptic auth error, not a startup error.langgraph up when Docker Desktop is stopped gives Cannot connect to the Docker daemon. Start Docker Desktop first, or switch to langgraph dev for in-process mode.langgraph.json for you, check three things: (1) the file path in "graphs" exists on disk, (2) the variable after : is a compiled graph object (not a function), and (3) "dependencies" includes every local package your graph imports.Your Studio server is now running. The topology panel shows a placeholder graph. Studio's most useful features — replaying runs, inspecting state, resuming after interrupts — only activate once a real is registered.
A placeholder graph has no attached. Studio can't persist or replay runs. You'll see the graph shape but the timeline panel stays empty.
The next module walks you through compiling a real StateGraph with a and updating so Studio discovers it by name. Then the execution timeline panel comes alive.
Compile a LangGraph StateGraph with a checkpointer and register it in langgraph.json so Studio discovers it by name. You complete a partially-written graph registration and confirm the topology renders in the UI.
How to compile a LangGraph StateGraph with a checkpointer and register it in langgraph.json so LangGraph Studio discovers and renders it by name.
Why this matters: Without this wiring, Studio has no graph to display — this is the step that turns your code into a visible, inspectable agent topology.
Decision this forces: MemorySaver (in-memory) vs. SqliteSaver checkpointer — which persistence level do you need during debugging?
tells the CLI two things: which Python module to import, and which compiled graph object to expose by name. Without both, has no graph to render.
In module 1 you pointed the manifest at a placeholder. This module replaces that placeholder with a real that carries a — the piece that lets Studio track state across steps.
A is the runnable form of your — you get it by calling .compile() and optionally passing a .
The checkpointer writes a snapshot after every runs, keyed by . Studio reads those snapshots to populate the state inspector and the .
Without a checkpointer, Studio can still render the topology, but the state inspector stays empty and step-through controls are disabled.
from langgraph.graph import StateGraph, START, END from langgraph.checkpoint.memory import MemorySaver from typing import TypedDict class State(TypedDict): messages: list def my_node(state: State) -> State: return {"messages": state["messages"] + ["node ran"]} builder = StateGraph(State) builder.add_node("my_node", my_node) builder.add_edge(START, "my_node") builder.add_edge("my_node", END) graph = builder.compile(checkpointer=MemorySaver())
This is the minimal pattern: define a , add one , wire START → node → END, then compile with a .
The variable graph is the object that will point to by name.
graph is a CompiledStateGraph object ready to run. Without the checkpointer argument, the graph still compiles and runs, but Studio's state inspector shows no snapshots and step-through controls are greyed out — because there is nothing writing state after each node.
{
"dependencies": ["."],
"graphs": {
"my_agent": "./agent.py:graph"
}
}The graphs key maps a display name ("my_agent") to a module-path:variable string. The CLI imports agent.py and reads the graph variable — the exact name you used in Stage 1.
Save this file and Studio hot-reloads automatically; you should see my_agent appear in the graph selector within a few seconds.
Studio (via the CLI) tries to import the attribute graph from agent.py and raises an AttributeError because that name no longer exists. The graph selector stays empty. Fix: update langgraph.json to "./agent.py:agent_graph".
from langgraph.graph import StateGraph, START, END from langgraph.checkpoint.sqlite import SqliteSaver from typing import TypedDict class State(TypedDict): messages: list def my_node(state: State) -> State: return {"messages": state["messages"] + ["node ran"]} builder = StateGraph(State) builder.add_node("my_node", my_node) builder.add_edge(START, "my_node") builder.add_edge("my_node", END) # TODO: compile with a SqliteSaver that writes to "checkpoints.db"
Everything above the TODO is identical to Stage 1 — only the changes. Stop and write the missing line before revealing the answer.
graph = builder.compile(checkpointer=SqliteSaver.from_conn_string("checkpoints.db"))
Changed lines vs. Stage 1: the import (SqliteSaver instead of MemorySaver) and this compile call. SqliteSaver writes snapshots to checkpoints.db on disk, so Studio can reload the server mid-session and still show the full execution timeline for previous runs — MemorySaver would lose all of that on restart.
| Option | Survives process restart | Setup required | When to choose | Cost | Complexity |
|---|---|---|---|---|---|
| MemorySaver | No — RAM only; all thread state is gone when the server stops | None — one import, one instantiation | Use during local debugging and Studio exploration — zero setup, state lives in RAM. | Free; state lost on restart | None — import and instantiate, no config |
| SqliteSaver | Yes — writes to a local .db file; threads resume after restart | Minimal — pass a db path string; no external service needed | Use when you need state to survive a Studio hot-reload or a crash mid-debug session. | Free; persists to disk | Low — provide a file path; SQLite file created automatically |
Three failure patterns come up repeatedly when connecting a graph to :
AttributeError: module 'agent' has no attribute 'graph' in the CLI log; the topology panel stays blank. Fix: align the "module:variable" string exactly.graph = builder.compile(...) is inside a function or if __name__ == '__main__' block. The CLI imports the module but the variable is never assigned. Result: same AttributeError as above. Fix: move the compile call to module scope..compile() called without a checkpointer. The graph loads and the topology renders, but the state inspector shows nothing after a run — no snapshots were written. Easy to miss because no error is raised. Fix: always pass a when you need Studio's state panel.When AI generates your graph registration code, check these four things before relying on the Studio view:
compile() call is at module scope, not inside a function.add_node was called but the edge was omitted.With the topology confirmed, you're ready for module 3: pausing at a boundary with Studio's step-through controls and reading the full state dict in the inspector panel before and after each node runs.
Use Studio's step-through controls to pause at a node boundary, read the full state dict in the inspector panel, and compare state before and after a node runs. You supply the missing interrupt_before list in a provided graph snippet to make a specific node pausable.
How to pause a LangGraph graph at a node boundary using interrupt_before or interrupt_after, read the state snapshot in Studio's inspector, and resume from a saved checkpoint.
Why this matters: Lets you freeze your SEO page generator mid-run to inspect and correct the keyword list or any other state key before a node acts on it — without redeploying.
Decision this forces: interrupt_before vs. interrupt_after — which gives you the state you actually need to inspect?
Answer: a (which persists state at every step) and a (which scopes state to one conversation run). Without both, Studio has no snapshot to show.
This module builds on that foundation. Now that state is saved, you'll learn to pause execution at a specific node boundary and read what changed. The question: how do you freeze the graph mid-run to see state before a node touches it?
LangGraph Studio lets you pause a graph at a boundary using two compile-time flags: interrupt_before and interrupt_after. Each takes a list of node names and tells the runtime when to freeze.
interrupt_before=["node_name"] — pauses before the node runs. The state snapshot shows the input the node is about to receive. Use this to inspect or edit state before a node acts on it.interrupt_after=["node_name"] — pauses after the node completes. The snapshot shows the output the node just wrote. Use this to verify what a node produced before the next node consumes it.The key tradeoff: interrupt_before lets you catch and correct bad inputs; interrupt_after lets you catch bad outputs. If you need to diff what a node changed, you need both — or use Studio's before/after snapshot comparison.
Imagine your SEO page generator has three nodes: fetch_serp (pulls search results), score_keywords (ranks them), and write_page (drafts the H1 and body). You suspect write_page is receiving a stale keyword list — but you can't tell from the final output alone.
You add interrupt_before=["write_page"] at compile time. Studio now freezes the graph the moment score_keywords finishes. In the inspector panel you see the full state dict: keywords, serp_results, and page_title — all with their current values, before the writer touches them.
You spot that keywords is missing the top-ranked term. You edit the value directly in Studio's state editor, then click Resume. The writer node runs with the corrected list, and the generated page now includes the right H1. No code change, no re-deploy.
from langgraph.graph import StateGraph, START, END def fetch_serp(state): return {"serp_results": ["result_a", "result_b"]} def score_keywords(state): return {"keywords": ["seo", "geo"]} def write_page(state): return {"page_draft": f"H1: {state['keywords'][0]}"} graph = StateGraph(dict) graph.add_node("fetch_serp", fetch_serp) graph.add_node("score_keywords", score_keywords) graph.add_node("write_page", write_page) graph.add_edge(START, "fetch_serp") graph.add_edge("fetch_serp", "score_keywords") graph.add_edge("score_keywords", "write_page") graph.add_edge("write_page", END) app = graph.compile(checkpointer=checkpointer)
This graph compiles and runs fine — but it runs straight through all three nodes with no pause. Studio shows you the final state only; you can't inspect what write_page received.
You see the final state dict (serp_results, keywords, page_draft) after all nodes complete. You cannot see the intermediate state at the write_page boundary — there is no checkpoint snapshot between score_keywords finishing and write_page starting, so you have no way to diff what write_page received vs. what it produced.
app = graph.compile( checkpointer=checkpointer, interrupt_before=["write_page"], # <-- pause before writer runs ) config = {"configurable": {"thread_id": "seo-run-01"}} app.invoke({"page_title": "best seo tools 2025"}, config=config) # Execution pauses here — write_page has NOT run yet # Studio inspector now shows the pre-write_page state snapshot
Adding interrupt_before=["write_page"] to compile() is the only change from Stage 1. The graph now saves a checkpoint right before write_page and surfaces it in Studio's inspector panel, so you can read and edit the state before the writer acts.
You should see serp_results, keywords, and page_title — all written by the earlier nodes. The key page_draft will be missing (or None) because write_page has not run yet. That absence is exactly what confirms the interrupt fired at the right boundary.
# After inspecting state in Studio, you edited keywords in the UI. # Now resume from the saved checkpoint. config = {"configurable": {"thread_id": "seo-run-01"}} # TODO: call the right method on `app` to resume from the checkpoint. # The graph should continue from write_page with the edited state. # Hint 1: resuming does not re-invoke from START — it continues the thread. # Hint 2: the resume value for a simple interrupt_before is None. result = app.???({"resume": None}, config=config) print(result["page_draft"])
Stop — attempt the TODO before revealing. The surrounding code is complete; only the method name on app is missing. It must continue the existing thread, not start a new run.
Replace ??? with invoke.
result = app.invoke({"resume": None}, config=config)
Changed line: app.invoke() called again on the SAME thread_id — LangGraph detects the pending interrupt and continues from the write_page checkpoint rather than restarting. Passing {"resume": None} signals that no external value is being injected; the graph simply unblocks and runs write_page with the (now edited) state it already holds.
app.get_graph().nodes and copy the exact node name string.ValueError: No checkpointer set. The interrupt mechanism depends on persistence; without it, there is nowhere to save the pause point.interrupt_before matches a string in app.get_graph().nodes.compile().page_draft) is absent in the snapshot — its presence means the interrupt did not fire.With interrupts working and state snapshots readable, the next module shows you how to follow Studio's execution timeline. Find every tool invocation and LLM request that fired inside a node. Match each one back to the state it produced.
Read Studio's execution timeline to find each tool invocation and LLM request, match them to the node that fired them, and verify inputs and outputs. You complete a tracing exercise by identifying which tool call produced a malformed output in a provided trace screenshot description.
How to read LangGraph Studio's execution timeline to locate every tool call and LLM request, match them to the node that fired them, and verify that outputs were correctly merged into agent state.
Why this matters: When your SEO agent produces wrong or missing content, the timeline tells you in seconds whether the bug is in a tool, a model call, or a state merge — without adding print statements or re-running blind.
Decision this forces: Studio timeline vs. LangSmith trace — when does each give you more signal?
Answer: you added node names to interrupt_before. Pausing let you read the full state dict in the inspector panel.
That pause gave you a snapshot of at one point in time. Now you need something finer: a record of every and every that fired inside each , in order. That is what the gives you.
LangGraph Studio's lists every event as a vertical stack of rows, oldest at the top.
Each row belongs to one and carries a type label — tool_call or llm — plus a duration marker in milliseconds.
Clicking a row opens a detail pane with three tabs: Input (what was sent), Output (what came back), and State (agent state after the event merged its result).
Duration markers distinguish a slow (500 ms–5 s) from a slow (network or DB latency) without guessing.
agent, search_tool)llm for model requests, tool_call for tool invocationsYour SEO agent runs three nodes: planner → search_tool → writer. The final page is missing the keyword cluster the planner requested.
You open the and scan the rows. You see three events: an llm row under planner (312 ms), a tool_call row under search_tool (4 200 ms, yellow status), and an llm row under writer (890 ms).
The yellow status on search_tool is the signal. You click the row and open Output: the tool returned {"results": null} instead of a keyword list.
You open the State tab for the same row. You confirm state["keywords"] is null. The malformed output merged into agent state. So writer had nothing to work with. The LLM rows are clean; the bug lives in the tool.
# Studio exposes timeline events as dicts when you call the API event = { "node": "search_tool", "type": "tool_call", "duration_ms": 4200, "status": "partial", "input": {"query": "best meta-description length 2025"}, "output": {"results": None}, } print(event["node"], event["type"], event["status"]) # search_tool tool_call partial
Each timeline row maps to a dict with the same fields you see in the Studio UI panel.
Notice output["results"] is None — that is the malformed payload from the scenario above, now visible in code.
Output: 'search_tool tool_call partial'. Status 'partial' means the tool returned something but the payload was incomplete or malformed — not a hard crash, but not a clean success either. That is why the keyword list is None.
# Simulate how Studio's State tab shows post-merge agent state def check_state_after_tool(event, agent_state): if event["type"] == "tool_call": # Studio merges output into state under the tool's key agent_state["keywords"] = event["output"].get("results") return agent_state state = {"keywords": [], "draft": ""} state = check_state_after_tool(event, state) print(state["keywords"]) # None ← writer node sees this
This mirrors what Studio's State tab shows: the tool's output is merged into agent state immediately after the event completes.
When state["keywords"] is None, every downstream node inherits the bad value — the bug propagates silently unless you check the State tab.
The writer node receives None. Its timeline row shows green (no crash) because the LLM accepted the null input and generated text anyway — a silent failure. You only catch it by checking the State tab on the search_tool row, not by waiting for an error.
events = [
{"node": "planner", "type": "llm", "duration_ms": 312, "status": "ok"},
{"node": "search_tool", "type": "tool_call", "duration_ms": 4200, "status": "partial"},
{"node": "writer", "type": "llm", "duration_ms": 890, "status": "ok"},
]
for e in events:
# TODO: print node + type for any event where duration_ms > 1000
# OR status != "ok" (flag both conditions)
passThis is a small variation of the Stage 2 example: same event list, new task — write the filter that surfaces the events worth investigating.
Stop — attempt the TODO before revealing the answer.
Replace pass with:
if e["duration_ms"] > 1000 or e["status"] != "ok":
print(e["node"], e["type"])
Changed lines: the if condition (the crux — combining duration and status thresholds).
Output:
search_tool tool_call
The planner row stays silent because 312 ms < 1000 and status is 'ok'. The writer row also stays silent for the same reason. Only search_tool triggers both conditions (4200 ms AND partial status) — exactly the event you'd click first in the Studio timeline.
| Option | State visibility | Cross-run history | When to choose | Cost | Complexity |
|---|---|---|---|---|---|
| Studio Timeline | Full agent state dict after every event, in the same panel | Current run only; no run-to-run comparison | Use Studio's timeline when you need to see live state merges, step through a single run interactively, or match a tool call to the exact node that fired it during active development. | Free with local Studio server | Low — built into Studio, zero config |
| LangSmith Trace | Span inputs/outputs only; no merged state dict view | Full run history, filtering, and latency dashboards | Use LangSmith when you need to compare latency or error rates across many runs, share a trace URL with a teammate, or audit production traffic. | Free tier available; paid for high volume | Medium — requires LANGCHAIN_API_KEY and tracing env vars |
{"results": null}. Status shows yellow, but downstream LLM rows stay green. You miss it unless you open State on the tool row.tool_call. Always read type first; network latency in a tool is a different fix than a slow prompt.tool_call row appears, the tool raised an exception before Studio logged it. Check the node's error border in the graph view and server logs for the stack trace.status AND duration_ms? Checking only one misses half the failure surface.event["output"] before assuming the key exists? A missing key raises KeyError on error events..get() with a safe default? Trusting the tool always returns the expected key causes silent null merges.Module 5 picks up here: once you've located a bad event in the timeline, Studio's red node borders, cycle counters, and state diffs give you visual signals to diagnose why it failed — infinite loops, corrupted state, and tool errors all leave distinct marks.
Use Studio's visual signals — red node borders, cycle counters, and state diffs — to diagnose infinite loops, corrupted state, and tool errors, then apply targeted fixes. You solo-debug a provided broken agent by reading its Studio output and writing the corrective code change.
How to use LangGraph Studio's visual signals — red node borders, cycle counters, and state diffs — to diagnose and fix infinite loops, corrupted state, and tool errors.
Why this matters: Debugging is the highest-friction part of building an SEO agent in LangGraph; knowing exactly which signal maps to which failure cuts hours of guesswork.
Decision this forces: Fix-and-replay from checkpoint vs. full restart — which is faster for validating a targeted fix?
Answer: the panel. It shows each and linked to the node that produced them. You can read both inputs sent and outputs returned.
That tracing skill is your entry point here. This module asks: once you spot a bad output, how do you diagnose why the graph went wrong? How do you fix it without re-running everything from scratch?
surfaces three visual signals. They map directly to the three most common agent failures.
Each signal answers a different question: where did it crash, how long did it spin, and what did it corrupt. Reading all three together gives you a complete diagnosis before you touch any code.
You open on an SEO agent. It generates page titles, scores them, and rewrites low-scoring ones. The run never finishes.
score: 0.41. At Step 5 it still shows score: 0.41. The rewrite node runs but the score never improves.score > 0.7) is never met.retry_count key to the . Exit after 3 rewrites regardless of score.You don't restart the whole run. Instead, replay from the snapshot at Step 2. That's the last clean state before the loop began. You validate the fix in seconds.
def should_rewrite(state): # Routes back to rewrite_node if score is too low if state["score"] < 0.7: return "rewrite_node" return END graph.add_conditional_edges( "score_node", should_rewrite, )
This conditional edge is the loop's engine — it routes back to rewrite_node whenever the score stays below 0.7, with no upper bound on retries.
The cycle counter reaches Step 10+ and keeps climbing. There is no exit path — should_rewrite always returns "rewrite_node" because the score condition is never satisfied. The graph never reaches END. Studio shows no red border because no exception is raised; the only signal is the climbing step counter.
# State schema gains a retry counter class AgentState(TypedDict): score: float retry_count: int # NEW def should_rewrite(state): if state["score"] >= 0.7 or state["retry_count"] >= 3: return END return "rewrite_node" # Replay from the clean checkpoint, not a full restart result = graph.invoke( None, # resume — state loaded from checkpoint config={"configurable": {"thread_id": "seo-run-01", "checkpoint_id": "step-2"}}, )
Two changes fix the loop: retry_count in the provides the upper bound, and replaying from checkpoint_id: "step-2" means you validate the fix without re-running the expensive LLM calls that preceded the loop.
retry_count: 2, score: 0.41 (or whatever the rewrite produces). At Step 7 the diff shows retry_count: 3 — and should_rewrite now returns END, so the cycle counter stops. The graph terminates cleanly instead of looping forever.
# Variation: the score node writes a string instead of a float def score_node(state): raw = call_scorer(state["title"]) return {"score": raw} # BUG: raw is "0.41" (str), not 0.41 (float) def should_rewrite(state): # TODO: add a guard so a string score doesn't cause a TypeError # Hint 1: convert state["score"] to float before comparing # Hint 2: wrap the comparison in try/except and return END on error if state["score"] >= 0.7 or state["retry_count"] >= 3: return END return "rewrite_node"
Stop — attempt the TODO before revealing. The state diff in Studio shows score: "0.41" (a string). Your task: fix should_rewrite so it handles the corrupted type without crashing, then confirm the fix by replaying from the last clean checkpoint.
TypeError: '>=' not supported between instances of 'str' and 'float' — Studio shows this in the red-bordered node's inspector panel.
Changed lines (the crux):
score = float(state["score"]) # coerce before compare — NEW
if score >= 0.7 or state["retry_count"] >= 3:
return END
Alternatively with try/except:
try:
if float(state["score"]) >= 0.7 or state["retry_count"] >= 3:
return END
except (ValueError, TypeError):
return END
After the fix, replay from the checkpoint just before score_node ran. The state diff should show score: 0.41 (float) and no TypeError in the inspector.
results: "ok" instead of results: ["ok"]. Trace back through diffs to the node whose step first shows the wrong type.When an AI tool generates a fix for a loop or corrupted state, check four things before trusting it.
END under realistic state values, not just in theory.retry_count), verify the declares it with a default. Otherwise existing checkpoints will KeyError on replay.With these checks done, you're ready for the lesson's solo capstone. A broken agent is handed to you cold. You diagnose and fix it entirely from Studio's signals and your own code changes.
Before reading the build order below, try to reconstruct it from memory: what is the first thing you must have running, what do you register next, and in what order do you move from state inspection to tracing to debugging? Write the five steps from memory, then compare.
Apply what you learned to SEO/GEO page — the page title is the H1 and the exact search query. OPEN with a 40-60 word self-contained, quotable answer capsule that names the entity explicitly in sentence one (no "in this lesson…" preamble) — this is the sentence an LLM lifts. Name the entity by its full name every time (not pronoun-only). Structure each section as ONE sub-question (How it works · X vs Y · When to use it · Example · Common mistakes), each opening with its own 1-2 sentence direct answer then depth. End with a genuine 3-5 question FAQ phrased as real user questions ending in "?" (these become FAQPage schema + the knowledge check). Prefer quotable specifics — concrete numbers, defaults, version names, one short snippet — over vague prose. Self-contained for a reader who arrived cold from a search engine or an LLM. Intent = explainer: mechanism + example + when-to-use. Target query: "langgraph studio"..
Which single command starts the LangGraph Studio dev server after langgraph-cli is installed?
The correct command is 'langgraph dev', which launches the in-process development server and opens the Studio UI. 'langgraph serve --reload' is not a real CLI flag. 'langchain studio --start' conflates LangChain and LangGraph CLIs. 'python -m langgraph.server' is not the documented entry point — the CLI wrapper 'langgraph dev' is.
You are debugging a multi-step agent locally and only need state to survive within a single debug session — you do not need persistence across restarts. Which checkpointer should you use?
MemorySaver stores checkpoints in RAM for the lifetime of the process, which is exactly what you need when debugging within a single session. SqliteSaver is not inherently faster — it writes to disk, adding I/O overhead. The node-count limit is invented; MemorySaver has no such restriction. MemorySaver works fully with LangGraph Studio — the Studio UI reads checkpoints regardless of which saver produced them.
Consider this graph configuration snippet:
graph.compile(interrupt_before=["call_tool"], checkpointer=MemorySaver())
At which point does execution pause?
interrupt_before pauses execution immediately before the named node executes, giving you the state snapshot as it exists at that moment — before any mutation by that node. interrupt_after would pause after the node runs and its output is merged. The list passed to interrupt_before is selective, not global. The pause is unconditional, not exception-triggered.
When would you reach for the LangGraph Studio timeline over a LangSmith trace to debug an agent problem?
LangGraph Studio's timeline is designed for local, interactive debugging — you can pause at a node, edit state, and replay from a saved checkpoint without restarting. Sharing a trace URL, comparing production latency at scale, and debugging remote deployments are all scenarios where LangSmith's hosted tracing gives more signal, because it persists traces centrally and works without a local dev server.
Your agent is stuck in a loop. In LangGraph Studio, what specific indicator tells you a loop is occurring, and what should you look for in the graph to find the root cause?
LangGraph Studio displays a cycle counter that rises each time the same node is revisited. A rising counter on one node signals a loop. The fix requires finding the conditional edge that should route execution out of the loop and checking why its predicate keeps returning the looping branch — usually a state key that is never updated to the value the exit condition checks.