22 KiB
Can Noodl Build the New Agentic Erleah?
Strategic Analysis & Roadmap
Date: December 30, 2025
Author: Strategic Analysis
Status: Recommendation - YES, with Phase 3.5 additions
Executive Summary
TL;DR: Yes, Noodl CAN build the new agentic Erleah, but it requires adding a focused "Phase 3.5: Real-Time Agentic UI" series of nodes and features. This is actually a PERFECT test case for Noodl's capabilities and would result in features that benefit the entire Noodl ecosystem.
Key Insights:
- Foundation is solid - Phases 1 & 2 created a modern React 19 + TypeScript base
- Core patterns exist - HTTP nodes, state management, and event systems are already there
- Missing pieces are specific - SSE streams, optimistic updates, and action dispatching
- High ROI - Building these features makes Noodl better for ALL modern web apps
- Validation opportunity - If Noodl can build Erleah, it proves the platform's maturity
Current Capabilities Assessment
✅ What Noodl ALREADY Has
1. Modern Foundation (Phase 1 Complete)
- React 19 in both editor and runtime
- TypeScript 5 with full type inference
- Modern tooling - webpack 5, Storybook 8
- Performance - Build times improved, hot reload snappy
2. HTTP & API Integration (Phase 2 In Progress)
// Current HTTP Node capabilities:
- ✅ GET/POST/PUT/DELETE/PATCH methods
- ✅ Authentication presets (Bearer, Basic, API Key)
- ✅ JSONPath response mapping
- ✅ Header and query parameter management
- ✅ Form data and URL-encoded bodies
- ✅ Timeout configuration
- ✅ Cancel requests
3. State Management
- ✅ Variable nodes for local state
- ✅ Object/Array manipulation nodes
- ✅ Component Inputs/Outputs for prop drilling
- ✅ Send Event/Receive Event for pub-sub
- ✅ States node for state machines
4. Visual Components
- ✅ Full React component library
- ✅ Responsive breakpoints (planned in NODES-001)
- ✅ Visual states (hover, pressed, disabled)
- ✅ Conditional rendering
- ✅ Repeater for dynamic lists
5. Event System
- ✅ Signal-based event propagation
- ✅ EventDispatcher for pub-sub patterns
- ✅ Connection-based data flow
- ✅ Debounce/Delay nodes for timing
❌ What Noodl Is MISSING for Erleah
1. Server-Sent Events (SSE) Support
Current Gap: HTTP node only does request-response, no streaming
Erleah Needs:
// Chat messages streaming in real-time
AI Agent: "I'm searching attendees..." [streaming]
AI Agent: "Found 8 matches..." [streaming]
AI Agent: "Adding to your plan..." [streaming]
What's Required:
- SSE connection node
- Stream parsing (JSON chunks)
- Progressive message accumulation
- Automatic reconnection on disconnect
2. WebSocket Support
Current Gap: No WebSocket node exists
Erleah Needs:
// Real-time bidirectional communication
User → Backend: "Add this to timeline"
Backend → User: "Timeline updated" [instant]
Backend → User: "Connection request accepted" [push]
3. Optimistic UI Updates
Current Gap: No pattern for "update UI first, sync later"
Erleah Needs:
// Click "Accept" → immediate UI feedback
// Then backend call → roll back if it fails
What's Required:
- Transaction/rollback state management
- Pending state indicators
- Error recovery patterns
4. Action Dispatcher Pattern
Current Gap: No concept of backend-triggered UI actions
Erleah Needs:
// Backend (AI Agent) sends:
{
type: "OPEN_VIEW",
view: "agenda",
id: "session-123"
}
// Frontend automatically navigates
What's Required:
- Action queue/processor
- UI action vocabulary
- Safe execution sandbox
5. State Synchronization Across Views
Current Gap: Component state is isolated, no global reactive store
Erleah Needs:
// Chat sidebar updates → Timeline view updates
// Timeline view updates → Parking Lot updates
// All views stay in sync automatically
What's Required:
- Global observable store (like Zustand)
- Subscription mechanism
- Selective re-rendering
Gap Analysis: Erleah Requirements vs Noodl Capabilities
Feature Comparison Matrix
| Erleah Feature | Noodl Today | Gap Size | Effort to Add |
|---|---|---|---|
| Timeline View | ✅ Repeater + Cards | None | 0 days |
| Chat Sidebar | ✅ Components | None | 0 days |
| Parking Lot Sidebar | ✅ Components | None | 0 days |
| Card Layouts | ✅ Visual nodes | None | 0 days |
| HTTP API Calls | ✅ HTTP Node | None | 0 days |
| Authentication | ✅ Auth presets | None | 0 days |
| SSE Streaming | ❌ None | Large | 3-5 days |
| WebSocket | ❌ None | Large | 3-5 days |
| Optimistic Updates | ❌ None | Medium | 2-3 days |
| Action Dispatcher | ⚠️ Partial | Medium | 2-4 days |
| Global State | ⚠️ Workarounds | Small | 2-3 days |
| State History | ❌ None | Small | 1-2 days |
| Real-time Preview | ✅ Existing | None | 0 days |
Total New Development: ~15-24 days
Proposed Phase 3.5: Real-Time Agentic UI
Insert this between current Phase 3 and the rest of the roadmap.
Task Series: AGENT (AI Agent Integration)
Total Estimated: 15-24 days (3-4 weeks)
| Task ID | Name | Estimate | Description |
|---|---|---|---|
| AGENT-001 | Server-Sent Events Node | 3-5 days | SSE connection, streaming, auto-reconnect |
| AGENT-002 | WebSocket Node | 3-5 days | Bidirectional real-time communication |
| AGENT-003 | Global State Store | 2-3 days | Observable store like Zustand, cross-component |
| AGENT-004 | Optimistic Update Pattern | 2-3 days | Transaction wrapper, rollback support |
| AGENT-005 | Action Dispatcher | 2-4 days | Backend-to-frontend command execution |
| AGENT-006 | State History & Time Travel | 1-2 days | Undo/redo, state snapshots |
| AGENT-007 | Stream Parser Utilities | 2-3 days | JSON streaming, chunk assembly |
AGENT-001: Server-Sent Events Node
File: packages/noodl-runtime/src/nodes/std-library/data/ssenode.js
var SSENode = {
name: 'net.noodl.SSE',
displayNodeName: 'Server-Sent Events',
docs: 'https://docs.noodl.net/nodes/data/sse',
category: 'Data',
color: 'data',
searchTags: ['sse', 'stream', 'server-sent', 'events', 'realtime'],
initialize: function() {
this._internal.eventSource = null;
this._internal.isConnected = false;
this._internal.messageBuffer = [];
},
inputs: {
url: {
type: 'string',
displayName: 'URL',
group: 'Connection',
set: function(value) {
this._internal.url = value;
}
},
connect: {
type: 'signal',
displayName: 'Connect',
group: 'Actions',
valueChangedToTrue: function() {
this.doConnect();
}
},
disconnect: {
type: 'signal',
displayName: 'Disconnect',
group: 'Actions',
valueChangedToTrue: function() {
this.doDisconnect();
}
},
autoReconnect: {
type: 'boolean',
displayName: 'Auto Reconnect',
group: 'Connection',
default: true
},
reconnectDelay: {
type: 'number',
displayName: 'Reconnect Delay (ms)',
group: 'Connection',
default: 3000
}
},
outputs: {
message: {
type: 'object',
displayName: 'Message',
group: 'Data'
},
data: {
type: '*',
displayName: 'Parsed Data',
group: 'Data'
},
connected: {
type: 'signal',
displayName: 'Connected',
group: 'Events'
},
disconnected: {
type: 'signal',
displayName: 'Disconnected',
group: 'Events'
},
error: {
type: 'string',
displayName: 'Error',
group: 'Events'
},
isConnected: {
type: 'boolean',
displayName: 'Is Connected',
group: 'Status',
getter: function() {
return this._internal.isConnected;
}
}
},
methods: {
doConnect: function() {
if (this._internal.eventSource) {
this.doDisconnect();
}
const url = this._internal.url;
if (!url) {
this.setError('URL is required');
return;
}
try {
const eventSource = new EventSource(url);
this._internal.eventSource = eventSource;
eventSource.onopen = () => {
this._internal.isConnected = true;
this.flagOutputDirty('isConnected');
this.sendSignalOnOutput('connected');
};
eventSource.onmessage = (event) => {
this.handleMessage(event);
};
eventSource.onerror = (error) => {
this._internal.isConnected = false;
this.flagOutputDirty('isConnected');
this.sendSignalOnOutput('disconnected');
if (this._internal.autoReconnect) {
setTimeout(() => {
if (!this._internal.eventSource || this._internal.eventSource.readyState === EventSource.CLOSED) {
this.doConnect();
}
}, this._internal.reconnectDelay || 3000);
}
};
} catch (e) {
this.setError(e.message);
}
},
doDisconnect: function() {
if (this._internal.eventSource) {
this._internal.eventSource.close();
this._internal.eventSource = null;
this._internal.isConnected = false;
this.flagOutputDirty('isConnected');
this.sendSignalOnOutput('disconnected');
}
},
handleMessage: function(event) {
try {
// Try to parse as JSON
const data = JSON.parse(event.data);
this._internal.message = event;
this._internal.data = data;
} catch (e) {
// Not JSON, use raw data
this._internal.message = event;
this._internal.data = event.data;
}
this.flagOutputDirty('message');
this.flagOutputDirty('data');
},
setError: function(message) {
this._internal.error = message;
this.flagOutputDirty('error');
},
_onNodeDeleted: function() {
this.doDisconnect();
}
}
};
module.exports = {
node: SSENode
};
AGENT-003: Global State Store Node
File: packages/noodl-runtime/src/nodes/std-library/data/globalstorenode.js
// Global store instance (singleton)
class GlobalStore {
constructor() {
this.stores = new Map();
this.subscribers = new Map();
}
createStore(name, initialState = {}) {
if (!this.stores.has(name)) {
this.stores.set(name, initialState);
this.subscribers.set(name, new Set());
}
return this.stores.get(name);
}
getState(name) {
return this.stores.get(name) || {};
}
setState(name, updates) {
const current = this.stores.get(name) || {};
const next = { ...current, ...updates };
this.stores.set(name, next);
this.notify(name, next);
}
subscribe(name, callback) {
if (!this.subscribers.has(name)) {
this.subscribers.set(name, new Set());
}
this.subscribers.get(name).add(callback);
// Return unsubscribe function
return () => {
this.subscribers.get(name).delete(callback);
};
}
notify(name, state) {
const subscribers = this.subscribers.get(name);
if (subscribers) {
subscribers.forEach(cb => cb(state));
}
}
}
const globalStoreInstance = new GlobalStore();
var GlobalStoreNode = {
name: 'net.noodl.GlobalStore',
displayNodeName: 'Global Store',
category: 'Data',
color: 'data',
initialize: function() {
this._internal.storeName = 'default';
this._internal.unsubscribe = null;
},
inputs: {
storeName: {
type: 'string',
displayName: 'Store Name',
default: 'default',
set: function(value) {
if (this._internal.unsubscribe) {
this._internal.unsubscribe();
}
this._internal.storeName = value;
this.setupSubscription();
}
},
set: {
type: 'signal',
displayName: 'Set',
valueChangedToTrue: function() {
this.doSet();
}
},
key: {
type: 'string',
displayName: 'Key'
},
value: {
type: '*',
displayName: 'Value'
}
},
outputs: {
state: {
type: 'object',
displayName: 'State',
getter: function() {
return globalStoreInstance.getState(this._internal.storeName);
}
},
stateChanged: {
type: 'signal',
displayName: 'State Changed'
}
},
methods: {
setupSubscription: function() {
const storeName = this._internal.storeName;
this._internal.unsubscribe = globalStoreInstance.subscribe(
storeName,
(newState) => {
this.flagOutputDirty('state');
this.sendSignalOnOutput('stateChanged');
}
);
// Trigger initial state
this.flagOutputDirty('state');
},
doSet: function() {
const key = this._internal.key;
const value = this._internal.value;
const storeName = this._internal.storeName;
if (key) {
globalStoreInstance.setState(storeName, { [key]: value });
}
},
_onNodeDeleted: function() {
if (this._internal.unsubscribe) {
this._internal.unsubscribe();
}
}
}
};
AGENT-005: Action Dispatcher Node
File: packages/noodl-runtime/src/nodes/std-library/data/actiondispatchernode.js
var ActionDispatcherNode = {
name: 'net.noodl.ActionDispatcher',
displayNodeName: 'Action Dispatcher',
category: 'Events',
color: 'purple',
initialize: function() {
this._internal.actionHandlers = new Map();
this._internal.pendingActions = [];
},
inputs: {
action: {
type: 'object',
displayName: 'Action',
set: function(value) {
this._internal.currentAction = value;
this.dispatch(value);
}
},
// Register handlers
registerHandler: {
type: 'signal',
displayName: 'Register Handler',
valueChangedToTrue: function() {
this.doRegisterHandler();
}
},
handlerType: {
type: 'string',
displayName: 'Handler Type'
},
handlerCallback: {
type: 'signal',
displayName: 'Handler Callback'
}
},
outputs: {
actionType: {
type: 'string',
displayName: 'Action Type'
},
actionData: {
type: 'object',
displayName: 'Action Data'
},
dispatched: {
type: 'signal',
displayName: 'Dispatched'
}
},
methods: {
dispatch: function(action) {
if (!action || !action.type) return;
this._internal.actionType = action.type;
this._internal.actionData = action.data || {};
this.flagOutputDirty('actionType');
this.flagOutputDirty('actionData');
this.sendSignalOnOutput('dispatched');
// Execute registered handlers
const handler = this._internal.actionHandlers.get(action.type);
if (handler) {
handler(action.data);
}
},
doRegisterHandler: function() {
const type = this._internal.handlerType;
if (!type) return;
this._internal.actionHandlers.set(type, (data) => {
this._internal.actionData = data;
this.flagOutputDirty('actionData');
this.sendSignalOnOutput('handlerCallback');
});
}
}
};
Implementation Strategy: Phases 1-2-3.5-3-4-5
Revised Roadmap
Phase 1: Foundation ✅ COMPLETE
├─ React 19 migration
├─ TypeScript 5 upgrade
└─ Storybook 8 migration
Phase 2: Core Features ⚙️ IN PROGRESS
├─ HTTP Node improvements ✅ COMPLETE
├─ Responsive breakpoints 🔄 ACTIVE
├─ Component migrations 🔄 ACTIVE
└─ EventDispatcher React bridge ⚠️ BLOCKED
Phase 3.5: Real-Time Agentic UI 🆕 PROPOSED
├─ AGENT-001: SSE Node (3-5 days)
├─ AGENT-002: WebSocket Node (3-5 days)
├─ AGENT-003: Global State Store (2-3 days)
├─ AGENT-004: Optimistic Updates (2-3 days)
├─ AGENT-005: Action Dispatcher (2-4 days)
├─ AGENT-006: State History (1-2 days)
└─ AGENT-007: Stream Utilities (2-3 days)
Total: 15-24 days (3-4 weeks)
Phase 3: Advanced Features
├─ Dashboard UX (DASH series)
├─ Git Integration (GIT series)
├─ Shared Components (COMP series)
├─ AI Features (AI series)
└─ Deployment (DEPLOY series)
Critical Path for Erleah
To build Erleah, this is the minimum required path:
Week 1-2: Phase 3.5 Core
- AGENT-001 (SSE) - Absolutely critical for streaming AI responses
- AGENT-003 (Global Store) - Required for synchronized state
- AGENT-007 (Stream Utils) - Need to parse SSE JSON chunks
Week 3: Phase 3.5 Enhancement
- AGENT-004 (Optimistic Updates) - Better UX for user interactions
- AGENT-005 (Action Dispatcher) - AI agent can control UI
Week 4: Erleah Development
- Build Timeline view
- Build Chat sidebar with SSE
- Build Parking Lot sidebar
- Connect to backend
Total: 4 weeks to validated Erleah prototype in Noodl
Why This Is GOOD for Noodl
1. Validates Modern Architecture
Building a complex, agentic UI proves that Noodl's React 19 + TypeScript migration was worth it. This is a real-world stress test.
2. Features Benefit Everyone
SSE, WebSocket, and Global Store aren't "Erleah-specific" - every modern web app needs these:
- Chat applications
- Real-time dashboards
- Collaborative tools
- Live notifications
- Streaming data visualization
3. Competitive Advantage
Flutterflow, Bubble, Webflow - none have agentic UI patterns built in. This would be a differentiator.
4. Dogfooding
Using Noodl to build a complex AI-powered app exposes UX issues and missing features that users face daily.
5. Marketing Asset
"Built with Noodl" becomes a powerful case study. Erleah is a sophisticated, modern web app that competes with pure-code solutions.
Risks & Mitigations
Risk 1: "We're adding too much complexity"
Mitigation: Phase 3.5 features are optional. Existing Noodl projects continue working. These are additive, not disruptive.
Risk 2: "What if we hit a fundamental limitation?"
Mitigation: Start with Phase 3.5 AGENT-001 (SSE) as a proof-of-concept. If that works smoothly, continue. If it's a nightmare, reconsider.
Risk 3: "We're delaying Phase 3 features"
Mitigation: Phase 3.5 is only 3-4 weeks. The learnings will inform Phase 3 (especially AI-001 AI Project Scaffolding).
Risk 4: "SSE/WebSocket are complex to implement correctly"
Mitigation: Leverage existing libraries (EventSource is native, WebSocket is native). Focus on the Noodl integration layer, not reinventing protocols.
Alternative: Hybrid Approach
If pure Noodl feels too risky, consider:
Option A: Noodl Editor + React Runtime
- Build most of Erleah in Noodl
- Write 1-2 custom React components for SSE streaming in pure code
- Import as "Custom React Components" (already supported in Noodl)
Pros:
- Faster initial development
- No waiting for Phase 3.5
- Still validates Noodl for 90% of the app
Cons:
- Doesn't push Noodl forward
- Misses opportunity to build reusable features
Option B: Erleah 1.0 in Code, Erleah 2.0 in Noodl
- Ship current Erleah version in pure React
- Use learnings to design Phase 3.5
- Rebuild Erleah 2.0 in Noodl with Phase 3.5 features
Pros:
- No business risk
- Informs Phase 3.5 design with real requirements
- Validates Noodl with second implementation
Cons:
- Slower validation loop
- Two separate codebases to maintain initially
Recommendation
✅ Go Forward with Phase 3.5
Rationale:
- Timing is right - Phases 1 & 2 created the foundation
- Scope is focused - 7 tasks, 3-4 weeks, clear boundaries
- Value is high - Erleah validates Noodl, features benefit everyone
- Risk is manageable - Start with AGENT-001, can pivot if needed
📋 Action Plan
Immediate (Next 2 weeks):
- Create AGENT-001 (SSE Node) task document
- Implement SSE Node as proof-of-concept
- Build simple streaming chat UI in Noodl to test
- Evaluate: Did this feel natural? Were there blockers?
If POC succeeds (Week 3-4): 5. Complete AGENT-003 (Global Store) 6. Complete AGENT-007 (Stream Utils) 7. Build Erleah Timeline prototype in Noodl
If POC struggles: 8. Document specific pain points 9. Consider hybrid approach 10. Inform future node design
🎯 Success Criteria
Phase 3.5 is successful if:
- ✅ SSE Node can stream AI responses smoothly
- ✅ Global Store keeps views synchronized
- ✅ Building Erleah in Noodl feels productive, not painful
- ✅ The resulting app performs well (no visible lag)
- ✅ Code is maintainable (not a tangled node spaghetti)
Conclusion
Can Noodl build the new agentic Erleah?
Yes - but only with Phase 3.5 additions. Without SSE, Global Store, and Action Dispatcher patterns, you'd be fighting the platform. With them, Noodl becomes a powerful tool for building modern, reactive, AI-powered web apps.
Should you do it?
Yes - this is a perfect validation moment. You've invested heavily in modernizing Noodl. Now prove it can build something cutting-edge. If Noodl struggles with Erleah, that's valuable feedback. If it succeeds, you have a compelling case study and a suite of new features.
Timeline:
- Phase 3.5 Development: 3-4 weeks
- Erleah Prototype: 1-2 weeks
- Total to Validation: 4-6 weeks
This is a strategic investment that pays dividends beyond just Erleah.
Next Steps
- Review this document with the team
- Decide on approach: Full Phase 3.5, Hybrid, or Pure Code
- If Phase 3.5: Start with AGENT-001 task creation
- If Hybrid: Design the boundary between Noodl and custom React
- If Pure Code: Document learnings for future Noodl improvements
Question to answer: What would prove to you that Noodl CAN'T build Erleah? Define failure criteria upfront so you can pivot quickly if needed.