LangGraph + our abstraction: building ProvenMap's architectural intelligence agents
On this page
A note on this post. It was first published in August 2025, and the product has shipped since. The engineering described here — the type-safe layer we built over LangGraph — is unchanged, but some of the product surfaces it mentions were experiments that did not make it into the shipped product. Names and framing have been brought up to date; the technical content is as written.
How we solved type safety, human-AI collaboration, and visual workflow authoring in an agentic system.
Background
When we (a two-member team—my wife and I) decided to switch from Scala to Node.js/TypeScript for our backend in the middle of the project, while simultaneously moving from India to the UK, it was a difficult period in our lives. It was madness.
After working with Scala and Akka Persistence, and having built a workflow engine with a team of bright minds, and working on different workflow engines, LangGraph felt like a breath of fresh air due to its simplicity. Why did we switch from Scala to Node.js? As a two-member bootstrapped team (with small angel investment), we wanted the same stack across our entire platform—we also didn't want overlap with my previous company.
TL;DR: No regrets about switching to Node.js—huge thanks to LangGraph.
The problem we solved
We needed to build a platform where both technical and non-technical stakeholders gain visual access to their architecture. Like any modern platform, we chose to rely heavily on AI agents collaborating on complex software analysis workflows. The main challenge was building agents on-the-fly as users interact with their workspaces. The core challenges:
Dynamic composable nodes - We made nodes composable; composed nodes are persisted in the database
Predictable I/O for nodes - To make them composable, we built an I/O layer that leverages LangGraph's state annotation internally. We created abstractions—let's call them CNode and CEdge for discussion
Runtime type safety - Visually built workflows need compile-time guarantees
Human-AI collaboration - Seamless handoffs between AI analysis and human review, with resume-from-interrupt using shared schemas
Comprehensive I/O analysis - Created a comprehensive I/O analysis layer that returns error lists when CNodes and CEdges are used
Dynamic composition by humans - Non-technical users building type-safe workflows leveraging I/O
Verifiability for end users - Error handling and simple observability
Our key abstraction over LangGraph: explicit input/output resolution
"LangGraph as a framework, particularly state annotation, is a powerful feature that centralizes node state management. The persistence layer and human-in-the-loop were so elegantly designed, effectively solving many challenges."
However, to build a composable and type-safe workflow in our case, we introduced explicit input and output concepts for each node that are resolved before node execution and applied upon node completion. All these properties are ultimately persisted to and resolved from the state annotation system via an abstracted node execution layer, which serves as the interface through which the LangGraph execution engine interacts with our concrete, composable nodes.
"This approach preserves LangGraph's strengths while enabling the type safety and composability we needed."
The core pattern
TYPESCRIPT
// 1. Define typed interfaces with decorators@TypeSchema({name:'CodeAnalysisInput'})classCodeAnalysisInput{@Field(()=>String,{required:true})repositoryUrl!:string@Field(()=>[String],{required:true})analysisScope!:('architecture'|'security'|'performance')[]}@TypeSchema({name:'CodeAnalysisOutput'})classCodeAnalysisOutput{@Field(()=>[String],{required:true})architecturalPatterns!:string[]@Field(()=>Number,{min:0,max:1})confidence!:number}// 2. Abstract node execution with I/O resolutionconstcodeAnalysisNode=defineNode({name:branded('code-analysis'),inputClass:CodeAnalysisInput,outputClass:CodeAnalysisOutput,execute:async({input,context,services})=>{// Fully typed business logic - no LangGraph state handlingconstanalysisResults=awaitservices.llm.analyze(input.repositoryUrl,{scope:input.analysisScope,})return{output:{architecturalPatterns:analysisResults.patterns,confidence:analysisResults.confidence,},completionMessage:`Analysis completed with ${analysisResults.confidence}% confidence`,}},})
The magic: pre/post execution resolution
TYPESCRIPT
// Before node executes: resolve typed input from LangGraph's dynamic stateconstinput=GraphInstanceResolver.getInstance(state.annotations,// LangGraph's dynamic stateCodeAnalysisInput,// Your typed classnodeInstanceName,// Unique node identifierfallBackValueResolver,// Handle missing/default values)// After node executes: apply typed output back to dynamic stateconstnewAnnotations=GraphInstanceResolver.applyInstance(state.annotations,CodeAnalysisOutput,nodeInstanceName,executionResult.output,)
This gives us:
Compile-time type checking for business logic
Runtime validation through schema decorators
Dynamic workflow composition without losing type safety
Visual editor compatibility with full type information
"The real breakthrough was eliminating the impedance mismatch between typed business logic and LangGraph's dynamic state management while preserving the power of both approaches."
Real-world example: architecture review workflow
TYPESCRIPT
// 1. AI analyzes codebaseconstanalysisNode=defineNode({name:branded('codebase-analysis'),inputClass:CodebaseInput,outputClass:AnalysisResults,execute:async({input,services})=>{constresults=awaitservices.llm.analyzeCodebase(input)return{output:results,completionMessage:'Analysis complete'}},})// 2. Human review for high-risk findingsconstreviewNode=defineNode({name:branded('human-review'),inputClass:AnalysisResults,outputClass:ReviewDecision,execute:async({input,services})=>{// Check if human review is neededif(input.criticalIssues.length>0){// Create typed interrupt formconstreviewForm=awaitservices.interruptMediator.interrupt({data:newHumanReviewForm({findings:input.criticalIssues,recommendation:input.aiRecommendation,}),description:'Critical security issues found - human review required',})return{output:{approved:reviewForm.approved,feedback:reviewForm.comments,},completionMessage:`Review ${reviewForm.approved?'approved':'rejected'}`,}}// Auto-approve if no critical issuesreturn{output:{approved:true,feedback:'Auto-approved'},completionMessage:'Auto-approved - no critical issues',}},})// 3. Dynamic routing based on review outcome// Routing decisions itself are dynamic, there is service that take config/or dynamic functions and returns the next nodefunctionrouteAfterReview(state:GraphState,ctx:GraphContext):string{constdecision=routingResolver(state,ctx)returndecision?'implement-recommendations':'escalate-to-senior-architect'}
Key technical innovations
1. Branded types for domain safety
TYPESCRIPT
// Prevent mixing up similar string typestypeNodeName=Branded<string,'NodeName'>typeNodeInstanceName=Branded<string,'NodeInstanceName'>typeWorkspaceId=Branded<string,'WorkspaceId'>// Compile-time safety across the entire stackfunctionexecuteNode(name:NodeName,instance:NodeInstanceName){// TypeScript prevents passing wrong string types}
2. Schema strategy pattern across the platform
TYPESCRIPT
// Support both static schemas (known at compile time) and dynamic schemas (LLM-generated)typeSchemaStrategy=|{type:'static';schemaClass:new()=>any}|{type:'dynamic';schema:DynamicSchemaContainer}// Runtime validation for bothconstvalidator=SchemaStrategy.getValidator(nodeDefinition.inputStrategy)constvalidatedInput=awaitvalidator.validate(rawInput)
3. Human-in-the-loop forms
TYPESCRIPT
@TypeSchema({name:'SecurityReviewForm',renderingStrategy:'inline_editable',actions:[{type:'submit',value:'Approve',variant:'primary'},{type:'submit',value:'Request Changes',variant:'warning'},{type:'cancel',value:'Escalate',variant:'secondary'},],})classSecurityReviewForm{@Field(()=>Boolean,{description:'Approve security findings?'})approved!:boolean@Field(()=>String,{description:'Review comments',required:false})comments?:string@Field(()=>[String],{description:'Required changes before approval'})requiredChanges!:string[]}
4. Effect-based error handling
TYPESCRIPT
// Functional error handling with full context preservationconstresult=pipe(validateWorkflowInput(input),Effect.andThen((input)=>executeWorkflow(input)),Effect.catchAll((error)=>handleWorkflowError(error,context)),Effect.tap((result)=>logWorkflowSuccess(result)),)
Production features that matter
Resume from any point
TYPESCRIPT
// Checkpoint every node executionconstgraph=workflow.compile({checkpointer:newPostgresCheckpointer(db),})// Resume interrupted workflowsawaitgraph.resume(threadId,{// Override state if neededapproved:true,reviewComments:'Approved after discussion',})
Real-time collaboration
TYPESCRIPT
// WebSocket updates for collaborative editingwebsocketService.broadcast({type:'workflow_state_changed',nodeInstanceName,newState:sanitizedState,timestamp:Date.now(),})
Comprehensive observability
TYPESCRIPT
// Automatic tracing for every nodeconsttrace=createNodeEvent.complete(nodeName,nodeInstanceName,nodeKind,startTime,completionMessage,nestedGraphContext,)awaittracer.nodeTrace(trace,context)
What we learned
What worked well
Input/Output resolution eliminates the impedance mismatch between typed business logic and LangGraph's dynamic state
Schema decorators enable automatic UI generation and validation
Branded types catch entire categories of bugs at compile time
Human-in-the-loop interrupts feel natural when properly abstracted
"LangGraph's interrupt mechanism is elegantly designed—we just needed to bridge it with our type-safe form generation system."
Challenges we solved
Dynamic schema validation - solved with runtime Zod schema generation
Visual editor sync - solved with bidirectional state transformers
Type safety in dynamic graphs - solved with schema strategy pattern
Error recovery - solved with Effect library and comprehensive checkpointing
What we saw in our own testing
These are our own observations from building and exercising the system, not benchmarked claims:
Runtime type errors in node execution effectively disappeared once I/O resolution was in place
Workflow development got noticeably faster once schema-driven UI generation and the I/O analysis layer existed
Human-AI handoffs felt immediate, with state pushed over WebSockets rather than polled
"The combination of LangGraph's checkpointing with our type-safe abstractions created a system that's both developer-friendly and production-reliable."
Key takeaways for the LangGraph community
I've worked with many workflow engines—JBPM, Activiti, Camunda, etc. After facing their limitations, even built workflow engine from scratch using Scala and Akka-persistence. I can say confidently that LangGraph (as stateful orchestration framework) surprised me with its simplicity and LLM-focused approach. It feels like process engines were reinvented for the AI era. What they've built is brilliant. LangGraph doesn't limit you, I feel. What we built is living proof of that flexibility.
The key lessons:
Type safety is achievable in dynamic systems with the right resolution layer
Human-AI collaboration requires careful interrupt design and state management
Visual workflow builders can maintain type safety with proper schema management
The full system handles environment injection, nested graph execution, dynamic schema inference, and multi-tenant isolation.
Happy to dive deeper into any specific patterns that interest you.
Built with LangGraph.js, TypeScript, React Flow, and Effect.
Subscribe to the blog
Get the latest insights on architectural intelligence and platform updates.
Murali Krishna Ganta is the founder of ProvenMap, a software architecture intelligence platform. He previously built low-code platforms and worked on industrial digitalisation projects.
Code preserves what was built. It does not preserve why. AI coding agents make this gap worse, not better - they read the artifact and not the input. The fix is not better diagrams or longer system prompts. It is keeping the reasoning alive next to the code it explains.
Sequoia Capital identifies a $10 trillion services automation opportunity, but calls this the "steam engine moment"—the foundational AI technology exists, but we're still decades from systematic deployment. Early agent-first implementations show 40-85% efficiency gains, yet success requires architectural expertise concentrated among technical specialists. The companies that democratize this knowledge—making agent system design as accessible as ChatGPT—will become the Westinghouse of the cognitive revolution.
An engineering retrospective from 2025. We prototyped a type-safe agent-workflow builder, then cut it — because the harder question turned out to be the simpler one: what is actually in my system, and what will this change break?