- AI
- LangGraph
- Node.js
- Architecture
Building Production AI Agent Workflows with LangGraph.js: Lessons from the Field
Building an AI agent that works in a demo is easy. Building one that works reliably in production, handling edge cases, recovering from failures, maintaining conversation state across sessions, and giving useful responses under conditions you didn't anticipate, is not. This is a post about lessons from building production LangGraph.js agent workflows across two different systems: a cooperative finance platform (Sociale) and a WhatsApp-native farm operations platform (MavunoStudio).
Why You Need a Framework
The first instinct when integrating LLMs is to call the API directly. For simple use cases, a single prompt-response, a fixed template with variable substitution, document summarisation, this is the right call. You do not need an agent framework to classify support tickets or generate product descriptions.
The friction starts when your agent needs to:
- Call tools and branch based on results
- Maintain state across multiple turns of a conversation
- Handle parallel sub-tasks that need to be re-integrated
- Retry failed steps without rerunning the whole pipeline
- Resume a suspended workflow from the last stable checkpoint
Raw API calls give you none of this. You end up building it yourself, a custom state machine, a tool dispatcher, a retry mechanism, and building it worse than a framework purpose-built for this.
LangGraph models agent logic as a directed state graph. Each node is a function that processes state and returns an update. Edges can be conditional, routing to different nodes based on current state. The state is explicit and typed. The graph can be visualised, debugged, and streamed. This is the mental model that matches how multi-step agent workflows actually work.
State Is the Hard Part
The single biggest mistake in early LangGraph implementations is treating state as just message history. State is everything the agent needs to make decisions, and that almost always extends beyond chat messages.
In the Sociale admin reporting agent, the state tracks:
- The reporting period and scope the administrator requested
- Which data sources have been queried and what they returned
- Intermediate aggregations computed so far
- The current draft of each report section
- Which sections still need generation
If you model this as just messages, every node has to re-parse the conversation to understand where it is. If you model it as structured state, every node receives exactly what it needs and produces exactly what the next node expects. The compiler catches state shape mismatches before they reach production.
interface ReportState {
period: { start: Date; end: Date };
tenantId: string;
queriedSources: string[];
aggregations: Record<string, unknown>;
draftSections: Record<string, string>;
status: 'gathering' | 'drafting' | 'reviewing' | 'complete';
}Define your state type before writing any nodes. Everything else follows from it.
Tool Call Reliability
LLMs call tools imperfectly. They pass the wrong argument types, omit required fields, or reference tools that do not exist in the current context. Production agents need to handle this without crashing.
Two patterns that work:
Schema validation at the boundary. Every tool call goes through a validation layer before execution. If the arguments do not match the expected schema, return a structured error that the model can reason about and retry. Zod works well here, the schema is both runtime validation and self-documenting specification.
Retry with error context. When a tool call fails, do not propagate the error directly to the user. Return it to the agent with the original call and the specific failure reason. Most models will self-correct on the retry given precise failure information. Set a retry limit, three is usually enough, and degrade gracefully after that.
In practice, the majority of tool call failures are argument shape problems that the model corrects on first retry when the error message is specific enough. "Expected quantity_kg to be a number, received string '50kg'" fixes itself. "Tool call failed" does not.
Streaming and User Feedback
Users interacting with an agent via WhatsApp or a dashboard do not want to wait fifteen seconds for a complete response. LangGraph supports streaming at the node level, partial outputs can be sent as they are generated.
For WhatsApp (MavunoStudio), this means sending interim status messages while agents are working: "Checking current tomato prices in Kiambu county..." while the market intelligence agent runs. The same latency feels acceptable when the user receives progress updates versus silence.
For dashboard interfaces (Sociale), streaming the report draft section by section lets administrators start reviewing early sections while later ones are still generating. The perceived response time is the time to the first section, not the time to the complete report.
Checkpointing for Durability
Long-running workflows, anything spanning multiple API calls over thirty or more seconds, should be checkpointed. LangGraph's persistence layer saves the agent state after each node, resumable from any checkpoint if the workflow fails or is interrupted.
This matters more than it sounds. In a financial context, if a report generation agent fails halfway through because of an upstream timeout, you do not want to restart from scratch. You want to resume from the last successful node. Checkpointing also enables human-in-the-loop workflows: the agent pauses after a sensitive step and waits for human approval before continuing.
For MavunoStudio, checkpointing is what makes multi-session WhatsApp conversations work. When a farmer sends a follow-up message six hours after the previous one, the agent resumes with the full context of the earlier exchange. From the farmer's perspective, the conversation is continuous.
Intent Routing
Not every message needs the same agent. "How are tomato prices in Kiambu today?" needs the market intelligence agent. "Find me a buyer for 200kg of french beans" needs the buyer engagement agent. "I need to pay Kamau for last week's delivery" needs the payments agent.
The router, the entry point of the LangGraph graph, classifies intent and directs to the right sub-graph. The critical design decision: the router should be fast and cheap. It runs on every incoming message. A lightweight model with a structured few-shot prompt is preferable to the same model used for the actual agent work. In MavunoStudio, GPT-4o-mini with a five-example few-shot prompt handles intent classification reliably at a fraction of the cost of the full model.
Fallback and Human Escalation
Every production agent needs a degradation path. When an agent cannot complete a task, tool failure, ambiguous input, safety filter, repeated retry exhaustion, there needs to be graceful fallback:
- First: the agent asks for clarification in plain language
- Second: the system acknowledges the request and flags it for human review
- Third: the system provides a static useful response and logs the failure with full context
Human escalation is real infrastructure, not a feature for later. If a message is flagged for human review, there needs to be an interface where a human can see it, respond on behalf of the system, and mark it resolved. In MavunoStudio, this is a queue in the operator dashboard. Escalation rate is a product health metric, if it trends upward, something upstream broke.
When Not to Use Agents
Not every LLM interaction needs LangGraph. If you are doing a single classification, generating a fixed-format output, or answering a question with RAG over a static corpus, the overhead of a state graph adds complexity without benefit.
The heuristic: use agents when the workflow has meaningful branching, when state needs to persist across steps, or when tools are involved. Use direct API calls when you have a single input-to-output flow with no branching and no state.
The most common mistake is over-engineering with agents when a well-crafted prompt and a deterministic wrapper would have solved the problem in fifty lines.