The Agentic Leap — Part 9: LangGraph Deep Dive
LangGraph becomes much less intimidating when we view it as a durable state machine. Nodes perform bounded work, edges describe permitted movement, checkpoints save progress and interrupts pause for people or external input.
A useful graph makes execution easier to predict, recover and test.Your ASP.NET Core experience with state transitions, background workers and optimistic concurrency transfers directly. The graph runtime provides useful primitives; it does not remove the need for domain policy.
LangGraph is a low-level orchestration runtime for stateful, long-running agents and workflows. It becomes useful when control flow needs explicit nodes and edges, durable checkpoints, interrupts, replay and recovery. It should make behaviour easier to reason about—not hide a simple workflow inside a graph.
1. Think in state transitions
Our planning review has clear stages:
START -> load_scope -> retrieve_evidence -> assess
-> ask_user -> assess
-> prepare_proposal
-> human_review
-> execute_or_reject -> END
Store raw structured facts in state rather than formatted prompts.
from typing import TypedDict
class PlanningState(TypedDict):
execution_id: str
project_id: str
objective: str
evidence: list[dict]
findings: list[dict]
proposal: dict | None
approval: dict | None
errors: list[dict]
Each node reads state, performs one bounded responsibility and returns an update.
2. Build the graph explicitly
from langgraph.graph import StateGraph, START, END
builder = StateGraph(PlanningState)
builder.add_node("load_scope", load_scope)
builder.add_node("retrieve_evidence", retrieve_evidence)
builder.add_node("assess", assess)
builder.add_node("prepare_proposal", prepare_proposal)
builder.add_node("human_review", human_review)
builder.add_node("execute", execute)
builder.add_edge(START, "load_scope")
builder.add_edge("load_scope", "retrieve_evidence")
builder.add_edge("retrieve_evidence", "assess")
builder.add_conditional_edges("assess", route_assessment)
builder.add_edge("execute", END)
Routing functions should return a small set of named destinations. Keep policy decisions outside arbitrary model strings.
3. Node boundaries are recovery boundaries
Durable execution resumes at node boundaries. Smaller nodes improve observability and reduce repeated work, but too many nodes increase state and operational complexity. Isolate non-idempotent side effects so replay is safe.
If a node calls an external system before an interrupt, that side effect may occur again when the node resumes. Move the interrupt first or make the effect idempotent and record it durably.
4. Checkpointers and stores solve different problems
A checkpointer persists thread-scoped graph snapshots for continuity, human review, time travel and fault recovery. A store holds application-defined cross-thread memory. Most production applications may use both, but neither should become the BuildEstate Pro system of record.
graph = builder.compile(
checkpointer=durable_checkpointer,
store=governed_memory_store,
)
config = {"configurable": {"thread_id": execution_id}}
result = graph.invoke(initial_state, config)
Use a durable supported checkpointer for production. Apply retention because checkpoints can grow without bound and may contain sensitive context.
5. Interrupt for human decisions
from langgraph.types import interrupt
def human_review(state: PlanningState):
decision = interrupt({
"proposal": state["proposal"],
"evidence": evidence_summary(state),
"allowed": ["approve", "reject"]
})
return {"approval": validate_decision(decision, state)}
The value returned to the UI must be safe and sufficient. On resume, validate authenticated identity, permission, expiry and the proposal hash. Do not trust the resumed payload merely because the graph accepts it.
6. Error classes and retry policy
Transient dependency failures may use a bounded node retry. Model-correctable problems can return structured observations. Missing user information interrupts. Policy denials route to a terminal refusal. Unexpected programming errors should surface to operations.
Do not catch every exception and ask the model what to do. That converts infrastructure uncertainty into invented recovery.
7. Time travel is not business rollback
Checkpoint replay can help debugging or explore an alternative trajectory. It cannot undo an email, payment or database command already performed. Replaying from old state also risks stale permissions and data.
Production replay should default to simulation/read-only mode. Any new side effect requires fresh policy, current data and a new idempotency identity.
8. Subgraphs and multi-agent composition
A subgraph can encapsulate a specialist flow. Use it when the component has a coherent state contract and can be tested independently. Shared state should be intentionally mapped; do not give every subgraph the entire parent context.
Agent hand-offs are graph transitions, not magical conversations. Record ownership, input, output and termination. Bound recursion and fan-out.
9. Streaming and user experience
Stream meaningful progress events such as “retrieving approved evidence” or “waiting for planning-manager review,” not private model reasoning. Events may be retried or reconnected; give them stable identities and derive the authoritative UI state from persisted execution state.
Angular can poll or subscribe through an ASP.NET Core facade. The browser should not connect directly to internal agent storage or carry provider credentials.
10. Testing and operations
Test nodes as functions, routing tables as state transitions and compiled graphs with fake dependencies. Add recovery tests that fail between nodes, resume interrupts, duplicate commands and expire approvals. Evaluate model nodes with datasets.
Trace graph/node names, checkpoint IDs, state versions, tool calls and outcomes. Redact values before sending traces to external observability platforms.
11. LangGraph versus explicit .NET orchestration
Use LangGraph when its persistence, interrupts and graph composition reduce substantial custom work. An explicit C# state machine, durable functions or workflow engine may fit better when the team owns .NET operations and the flow is mostly deterministic. The right comparison includes support skills, data residency, deployment and failure recovery—not only demo code length.
Extended implementation review
12. Design nodes as replay boundaries
A node should perform one responsibility and return a state update. Pure assessment nodes are naturally replay-safe. External command nodes require idempotency and should often be split into prepare, dispatch and reconcile stages. Avoid hidden mutation in helper functions because checkpoint replay can call a node again.
Reducers determine how concurrent updates combine. Choose them explicitly for message lists, evidence sets and error collections. A last-write-wins default can silently discard one parallel branch. Keep state compact; store large artifacts elsewhere and reference them by protected identifier.
13. Routing and termination
Conditional edges should depend on typed fields such as assessment_status, not prose. Enumerate every route and provide a safe path for unknown values. Cycles need counters and evidence-change checks so repeated assessment cannot run forever.
Use explicit terminal outcomes: completed, insufficient evidence, input required, rejected, failed and cancelled. A graph reaching END does not automatically mean business success; the state must say which outcome occurred and why.
14. Checkpointers, threads and stores
The checkpointer saves graph state at steps, organised by a thread identifier. That enables resume, inspection and fault recovery. Use a production persistence backend with tenant isolation, encryption, retention and concurrency controls. Treat thread identifiers as opaque references, not authorisation.
The long-term store is separate. It can retain cross-thread information, but needs its own namespace, consent and deletion policy. Business records remain in authoritative services. A checkpoint may reference them; it should not become a shadow system of record.
15. Interrupt semantics
An interrupt saves state and returns a JSON-serialisable request to the caller. Resuming with a command re-enters the node, so code before the interrupt may execute again. Keep pre-interrupt work pure or idempotent, do not reorder interrupts casually and do not wrap them in broad exception handling.
For approval, persist the proposal digest and show it through the trusted UI. On resume, ASP.NET Core verifies the user, expiry, evidence freshness and command preconditions. The resume payload itself is untrusted input.
16. Subgraphs and multi-agent composition
Use a subgraph for a component with its own state and lifecycle, not merely to organise code. Pass the minimum fields required and decide whether its checkpoint namespace is per invocation or shared by thread. A worker subgraph should not inherit all parent tools or sensitive context.
Bound recursion and parallel fan-out. Correlate child runs and propagate cancellation. Validate child output before merging it into parent state, particularly when it crosses a trust boundary.
17. Time travel and side-effect safety
Checkpoint history helps debugging and can fork execution from an earlier state. It does not undo emails, database writes or remote tasks. Replaying from before a command can repeat the effect unless the command is idempotent or the fork is explicitly read-only.
Restrict time-travel operations to authorised operators, label forked runs and isolate them from production actions. Preserve the original audit history. Use recorded state to understand decisions, not to expose hidden model reasoning.
18. Deployment and schema evolution
Pin LangGraph and persistence dependencies. Version graph state and migrate old checkpoints or terminate them clearly. A deployment that renames a node or changes interrupt order can strand active runs, so test resume using fixtures from the current production version.
Run workers with leases and graceful shutdown. Stop claiming new work, complete or checkpoint safe boundaries and release leases. Monitor checkpoint latency, conflict rate, queue age, node failures and stuck interrupts.
19. Evaluation strategy
Test nodes as pure units where possible, route functions with exhaustive states and the compiled graph with fake tools/models. Inject crashes between nodes, duplicate resume calls, stale approvals and downstream unknown outcomes. Assert both correct results and the absence of duplicate side effects.
Model-node evaluations should measure decision and evidence quality. End-to-end evaluations measure terminal outcome, recovery, latency and cost. Trace node names and checkpoint versions so regressions are diagnosable.
20. Worked interrupt and recovery path
The graph reaches human_review with a persisted proposal. The node calls interrupt with a display-safe payload. Because resume re-enters the node, proposal construction occurred in an earlier pure node and the review node performs no command before the interrupt. The API returns status without holding an HTTP request open.
Hours later, the user approves. ASP.NET Core authenticates them, loads the execution and supplies a resume command. The node validates the response shape, but the protected command service independently verifies approval, resource access and proposal digest. If evidence has changed, the graph routes to reassessment rather than execution.
Suppose the command succeeds and the worker crashes before its next checkpoint. On replay, the command handler uses the same idempotency key, retrieves the existing business result and returns it. The graph reaches the same state without duplicating the task. This is why node boundaries, persistence and domain idempotency must be designed together.
21. Graph review questions
- Does every state field have an owner and retention purpose?
- Are route values typed and exhaustively handled?
- Can each node be retried without hidden effects?
- Do reducers preserve concurrent branch results?
- Are interrupts stable across deployed versions?
- Can time travel repeat a command or expose sensitive state?
- Are subgraphs scoped to minimum tools and data?
- Can an old checkpoint resume after an upgrade?
- Does streaming reveal only user-appropriate progress?
22. Common graph design mistakes
A graph with one enormous “agent” node gains little from explicit orchestration because decisions, tools and side effects remain hidden. Split at meaningful replay and policy boundaries. The opposite mistake is turning every helper into a node, creating noisy checkpoints and difficult migrations. A node should represent operationally significant work.
Do not store clients, database connections or arbitrary class instances in graph state. Persist serialisable domain data and protected references. Avoid copying the complete message history into every subgraph. Define adapters between parent and child schemas.
Static edges are preferable when control is deterministic. Use conditional routing only where state genuinely changes the path. Keep routing functions pure and covered by exhaustive tests. The resulting diagram should help an operator predict execution; if it merely mirrors implementation detail, simplify it.
Streaming needs its own contract. Emit stable event types for stage started, evidence found, input required, approval required, completed and failed. Clients reconnect using a cursor and tolerate duplicates. Token streaming is not durable progress: generated text may be revised, rejected by policy or lost before a checkpoint commits.
Document graph ownership. Name who can change nodes, state schemas, routing and persistence; identify who handles stuck runs; and establish retention for checkpoints and traces. A visual graph creates false confidence unless operational responsibilities are explicit and exercised.
Review those ownership responsibilities at every significant release and after incidents.
Production checklist
- Graph state is typed, minimal and versioned.
- Node boundaries align with safe replay points.
- Checkpointer and store have distinct governed purposes.
- Interrupt resume revalidates identity and proposal integrity.
- Retry behaviour is failure-specific and bounded.
- Time travel cannot silently repeat business effects.
- Subgraphs receive minimum necessary state.
- Streaming exposes progress, not private reasoning.
- Recovery paths are integration-tested.
23. Continue the series
The individual concepts are ready to assemble. Part 10 builds the production-minded ASP.NET Core capstone, including Angular review, SQL state, RAG, tools, approval, observability and evaluation.
A complete boundary around the Python graph
ASP.NET Core creates the BuildEstate Pro execution and remains the public API. It sends a bounded request to the graph service and exposes status to Angular. The browser never receives provider credentials or a serialized checkpoint.
app.MapPost("/agent-executions/{id:guid}/advance",
async (Guid id, ClaimsPrincipal user,
IAgentGraphClient graph,
IExecutionAuthorizer authorization,
CancellationToken ct) =>
{
var execution = await authorization.LoadAllowedAsync(user, id, ct);
var result = await graph.AdvanceAsync(
execution.ToGraphRequest(), ct);
return Results.Ok(result.ToDisplayModel());
});
The graph stores the minimum state needed for orchestration. Business records remain behind .NET APIs. Large evidence stays in protected storage and appears in graph state as a reference, version and digest.
When human_review calls interrupt, the graph checkpointer saves its place. Angular displays the proposal through ASP.NET Core. On resume, the application rechecks the reviewer, project permission, expiry and proposal digest before sending the decision. The command service repeats those checks.
Deploying with Microsoft Foundry and Azure
A LangGraph agent can be packaged as a hosted agent in Microsoft Foundry when you want managed hosting, scaling, agent identity and observability. Alternatively, run it in Azure Container Apps or AKS behind a private authenticated endpoint. Choose based on team skills, networking, state requirements and operational ownership.
Use a production checkpointer with encryption, tenant isolation, retention and tested restore. Azure SQL or PostgreSQL can support durable state depending on the selected integration. Service Bus can schedule or resume work, but duplicate messages must be safe. OpenTelemetry links graph nodes to ASP.NET Core and downstream services.
Deployment tests should resume checkpoints created by the previous application version. Changing a node name, state schema or interrupt order can strand active work. Version the state and provide migration or an explicit terminal outcome.
A gentle graph-design test
Read the diagram from left to right and explain each node without mentioning the framework. State its input, output, authority, side effects and retry behaviour. Then place a crash immediately after it.
If the next worker cannot determine what happened, move the checkpoint boundary or add idempotency. If one node performs retrieval, assessment, approval and command execution, split it. If every helper has become a node, simplify it. Good graph design sits between those extremes.
