Sunday, August 10, 2025
LangGraph + ContextDx abstraction: Building ContexDx Architectural intelligence agents


I'll help refine your blog post with grammar corrections and proper terminology. Here's the improved version:
LangGraph + Our Abstraction: Building ContextDX Architectural Intelligence Agent Builder
How we solved type safety, human-AI collaboration, and visual workflow authoring in an agentic system for our specialized needs of our efforts to "Democratisation of Architecture intelligence engine"
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 an architecture intelligence platform where both technical and non-technical stakeholders gain visual access to architectural intelligence. 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
- Dynamic composition by Agent Builder - As users interact with workspaces, we needed to build domain-specific agents on the fly and persist them in the customer's specific database. We used a specialized agent to achieve this—I/O analysis results were fed to the LLM, expecting mappers or CNode switching
- Verifiability for end users - Error handling and simple observability
Our Key Abstraction Over LangGraph for Backend: 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' }) class CodeAnalysisInput { @Field(() => String, { required: true }) repositoryUrl!: string @Field(() => [String], { required: true }) analysisScope!: ('architecture' | 'security' | 'performance')[] } @TypeSchema({ name: 'CodeAnalysisOutput' }) class CodeAnalysisOutput { @Field(() => [String], { required: true }) architecturalPatterns!: string[] @Field(() => Number, { min: 0, max: 1 }) confidence!: number } // 2. Abstract node execution with I/O resolution const codeAnalysisNode = defineNode({ name: branded('code-analysis'), inputClass: CodeAnalysisInput, outputClass: CodeAnalysisOutput, execute: async ({ input, context, services }) => { // Fully typed business logic - no LangGraph state handling const analysisResults = await services.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 state const input = GraphInstanceResolver.getInstance( state.annotations, // LangGraph's dynamic state CodeAnalysisInput, // Your typed class nodeInstanceName, // Unique node identifier fallBackValueResolver, // Handle missing/default values ) // After node executes: apply typed output back to dynamic state const newAnnotations = 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 codebase const analysisNode = defineNode({ name: branded('codebase-analysis'), inputClass: CodebaseInput, outputClass: AnalysisResults, execute: async ({ input, services }) => { const results = await services.llm.analyzeCodebase(input) return { output: results, completionMessage: 'Analysis complete' } }, }) // 2. Human review for high-risk findings const reviewNode = defineNode({ name: branded('human-review'), inputClass: AnalysisResults, outputClass: ReviewDecision, execute: async ({ input, services }) => { // Check if human review is needed if (input.criticalIssues.length > 0) { // Create typed interrupt form const reviewForm = await services.interruptMediator.interrupt({ data: new HumanReviewForm({ 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 issues return { 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 node function routeAfterReview(state: GraphState, ctx: GraphContext): string { const decision = routingResolver(state, ctx) return decision ? 'implement-recommendations' : 'escalate-to-senior-architect' }
Key Technical Innovations
1. Branded Types for Domain Safety
TYPESCRIPT// Prevent mixing up similar string types type NodeName = Branded<string, 'NodeName'> type NodeInstanceName = Branded<string, 'NodeInstanceName'> type WorkspaceId = Branded<string, 'WorkspaceId'> // Compile-time safety across the entire stack function executeNode(name: NodeName, instance: NodeInstanceName) { // TypeScript prevents passing wrong string types }
2. Schema Strategy Pattern across platform
TYPESCRIPT// Support both static schemas (known at compile time) and dynamic schemas (LLM-generated) type SchemaStrategy = | { type: 'static'; schemaClass: new () => any } | { type: 'dynamic'; schema: DynamicSchemaContainer } // Runtime validation for both const validator = SchemaStrategy.getValidator(nodeDefinition.inputStrategy) const validatedInput = await validator.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' }, ], }) class SecurityReviewForm { @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 preservation const result = 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 execution const graph = workflow.compile({ checkpointer: new PostgresCheckpointer(db), }) // Resume interrupted workflows await graph.resume(threadId, { // Override state if needed approved: true, reviewComments: 'Approved after discussion', })
Real-time Collaboration
TYPESCRIPT// WebSocket updates for collaborative editing websocketService.broadcast({ type: 'workflow_state_changed', nodeInstanceName, newState: sanitizedState, timestamp: Date.now(), })
Comprehensive Observability
TYPESCRIPT// Automatic tracing for every node const trace = createNodeEvent.complete( nodeName, nodeInstanceName, nodeKind, startTime, completionMessage, nestedGraphContext, ) await tracer.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
📈 Test Results
- Zero runtime type errors in node execution since implementing I/O resolution
- 30% faster workflow development due to automatic UI generation, IO analysis, comprehensive custom evals layer
- Human-AI handoffs take <2 seconds with real-time WebSocket updates
- 99.8% workflow resume success rate after interruptions
"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 engine were reinvented for the AI era. What they've built—and it's brilliant. LangGraph doesn't limit you I feel. What we built is living proof of that flexibility.
The key lessons:
- Don't fight LangGraph's patterns - extend them with thoughtful abstractions
- Type safety is achievable in dynamic systems with the right resolution layer
- Human-AI collaboration requires careful interrupt design and state management
- Production reliability We performed extensive tests, but not production. I will come back and comment on this further.
- 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. We're hoping to use this in production for architecture intelligence dashboards where non-technical users build complex AI-assisted workflows. Fingers crossed for the launch!
Happy to dive deeper into any specific patterns that interest you!
This post focuses on the technical architecture and type safety innovations. For details on the visual workflow builder that non-technical users interact with, see our companion post on building the drag-and-drop interface that compiles these visual designs into the type-safe execution graphs described here.
Built with LangGraph.js, TypeScript, React Flow, and Effect.