mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-03-08 10:03:31 +01:00
Phase 5 - BYOB Backend (TASK-007A/B): - LocalSQL Adapter with full CloudStore API compatibility - QueryBuilder translates Parse-style queries to SQL - SchemaManager with PostgreSQL/Supabase export - LocalBackendServer with REST endpoints - BackendManager with IPC handlers for Electron - In-memory fallback when better-sqlite3 unavailable Phase 3 - GitHub Panel (GIT-004): - Issues tab with list/detail views - Pull Requests tab with list/detail views - GitHub API client with OAuth support - Repository info hook integration Phase 3 - Editor UX Bugfixes (TASK-013): - Legacy runtime detection banners - Read-only enforcement for legacy projects - Code editor modal close improvements - Property panel stuck state fix - Blockly node deletion and UI polish Phase 11 - Cloud Functions Planning: - Architecture documentation for workflow automation - Execution history storage schema design - Canvas overlay concept for debugging Docs: Updated LEARNINGS.md and COMMON-ISSUES.md
CF11-001: Logic Nodes (IF/Switch/ForEach/Merge)
Metadata
| Field | Value |
|---|---|
| ID | CF11-001 |
| Phase | Phase 11 |
| Series | 1 - Advanced Workflow Nodes |
| Priority | 🟡 High |
| Difficulty | 🟡 Medium |
| Estimated Time | 12-16 hours |
| Prerequisites | Phase 5 TASK-007C (Workflow Runtime) |
| Branch | feature/cf11-001-logic-nodes |
Objective
Create advanced workflow logic nodes that enable conditional branching, multi-way routing, array iteration, and parallel execution paths - essential for building non-trivial automation workflows.
Background
Current Noodl nodes are designed for UI and data flow but lack the control-flow constructs needed for workflow automation. To compete with n8n/Zapier, we need:
- IF Node: Route data based on conditions
- Switch Node: Multi-way branching (like a switch statement)
- For Each Node: Iterate over arrays
- Merge Node: Combine multiple execution paths
Current State
- Basic condition node exists but isn't suited for workflows
- No iteration nodes
- No way to merge parallel execution paths
Desired State
- IF node with visual expression builder
- Switch node with multiple case outputs
- For Each node for array iteration
- Merge node to combine paths
- All nodes work in CloudRunner workflow context
Scope
In Scope
- IF Node implementation
- Switch Node implementation
- For Each Node implementation
- Merge Node implementation
- Property editor integrations
- CloudRunner execution support
Out of Scope
- UI runtime nodes (frontend-only)
- Visual expression builder (can use existing or defer)
Technical Approach
IF Node
Routes execution based on a boolean condition.
// packages/noodl-viewer-cloud/src/nodes/logic/IfNode.ts
const IfNode = {
name: 'Workflow IF',
displayName: 'IF',
category: 'Workflow Logic',
color: 'logic',
inputs: {
condition: {
type: 'boolean',
displayName: 'Condition',
description: 'Boolean expression to evaluate'
},
data: {
type: '*',
displayName: 'Data',
description: 'Data to pass through'
},
run: {
type: 'signal',
displayName: 'Run',
description: 'Trigger to evaluate condition'
}
},
outputs: {
onTrue: {
type: 'signal',
displayName: 'True',
description: 'Triggered when condition is true'
},
onFalse: {
type: 'signal',
displayName: 'False',
description: 'Triggered when condition is false'
},
data: {
type: '*',
displayName: 'Data',
description: 'Pass-through data'
}
},
run(context) {
const condition = context.inputs.condition;
context.outputs.data = context.inputs.data;
if (condition) {
context.triggerOutput('onTrue');
} else {
context.triggerOutput('onFalse');
}
}
};
Switch Node
Routes to one of multiple outputs based on value matching.
// packages/noodl-viewer-cloud/src/nodes/logic/SwitchNode.ts
const SwitchNode = {
name: 'Workflow Switch',
displayName: 'Switch',
category: 'Workflow Logic',
color: 'logic',
inputs: {
value: {
type: '*',
displayName: 'Value',
description: 'Value to switch on'
},
data: {
type: '*',
displayName: 'Data'
},
run: {
type: 'signal',
displayName: 'Run'
}
},
outputs: {
default: {
type: 'signal',
displayName: 'Default',
description: 'Triggered if no case matches'
},
data: {
type: '*',
displayName: 'Data'
}
},
// Dynamic outputs for cases - configured via property panel
dynamicports: {
outputs: {
cases: {
type: 'signal'
// Generated from cases array: case_0, case_1, etc.
}
}
},
setup(context) {
// Register cases from configuration
const cases = context.parameters.cases || [];
cases.forEach((caseValue, index) => {
context.registerOutput(`case_${index}`, {
type: 'signal',
displayName: `Case: ${caseValue}`
});
});
},
run(context) {
const value = context.inputs.value;
const cases = context.parameters.cases || [];
context.outputs.data = context.inputs.data;
const matchIndex = cases.indexOf(value);
if (matchIndex >= 0) {
context.triggerOutput(`case_${matchIndex}`);
} else {
context.triggerOutput('default');
}
}
};
For Each Node
Iterates over an array, executing the output for each item.
// packages/noodl-viewer-cloud/src/nodes/logic/ForEachNode.ts
const ForEachNode = {
name: 'Workflow For Each',
displayName: 'For Each',
category: 'Workflow Logic',
color: 'logic',
inputs: {
items: {
type: 'array',
displayName: 'Items',
description: 'Array to iterate over'
},
run: {
type: 'signal',
displayName: 'Run'
}
},
outputs: {
iteration: {
type: 'signal',
displayName: 'For Each Item',
description: 'Triggered for each item'
},
currentItem: {
type: '*',
displayName: 'Current Item'
},
currentIndex: {
type: 'number',
displayName: 'Index'
},
completed: {
type: 'signal',
displayName: 'Completed',
description: 'Triggered when iteration is complete'
},
allResults: {
type: 'array',
displayName: 'Results',
description: 'Collected results from all iterations'
}
},
async run(context) {
const items = context.inputs.items || [];
const results = [];
for (let i = 0; i < items.length; i++) {
context.outputs.currentItem = items[i];
context.outputs.currentIndex = i;
// Trigger and wait for downstream to complete
const result = await context.triggerOutputAndWait('iteration');
if (result !== undefined) {
results.push(result);
}
}
context.outputs.allResults = results;
context.triggerOutput('completed');
}
};
Merge Node
Waits for all input paths before continuing.
// packages/noodl-viewer-cloud/src/nodes/logic/MergeNode.ts
const MergeNode = {
name: 'Workflow Merge',
displayName: 'Merge',
category: 'Workflow Logic',
color: 'logic',
inputs: {
// Dynamic inputs based on configuration
},
outputs: {
merged: {
type: 'signal',
displayName: 'Merged',
description: 'Triggered when all inputs received'
},
data: {
type: 'object',
displayName: 'Data',
description: 'Combined data from all inputs'
}
},
dynamicports: {
inputs: {
branches: {
type: 'signal'
// Generated: branch_0, branch_1, etc.
},
branchData: {
type: '*'
// Generated: data_0, data_1, etc.
}
}
},
setup(context) {
const branchCount = context.parameters.branchCount || 2;
context._receivedBranches = new Set();
context._branchData = {};
for (let i = 0; i < branchCount; i++) {
context.registerInput(`branch_${i}`, {
type: 'signal',
displayName: `Branch ${i + 1}`
});
context.registerInput(`data_${i}`, {
type: '*',
displayName: `Data ${i + 1}`
});
}
},
onInputChange(context, inputName, value) {
if (inputName.startsWith('branch_')) {
const index = parseInt(inputName.split('_')[1]);
context._receivedBranches.add(index);
context._branchData[index] = context.inputs[`data_${index}`];
const branchCount = context.parameters.branchCount || 2;
if (context._receivedBranches.size >= branchCount) {
context.outputs.data = { ...context._branchData };
context.triggerOutput('merged');
// Reset for next execution
context._receivedBranches.clear();
context._branchData = {};
}
}
}
};
Key Files to Create
| File | Purpose |
|---|---|
nodes/logic/IfNode.ts |
IF node definition |
nodes/logic/SwitchNode.ts |
Switch node definition |
nodes/logic/ForEachNode.ts |
For Each node definition |
nodes/logic/MergeNode.ts |
Merge node definition |
nodes/logic/index.ts |
Module exports |
tests/logic-nodes.test.ts |
Unit tests |
Implementation Steps
Step 1: IF Node (3h)
- Create node definition
- Implement run logic
- Add to node registry
- Test with CloudRunner
Step 2: Switch Node (4h)
- Create node with dynamic ports
- Implement case matching logic
- Property editor for case configuration
- Test edge cases
Step 3: For Each Node (4h)
- Create node definition
- Implement async iteration
- Handle
triggerOutputAndWaitpattern - Test with arrays and objects
Step 4: Merge Node (3h)
- Create node with dynamic inputs
- Implement branch tracking
- Handle reset logic
- Test parallel paths
Step 5: Integration & Testing (2h)
- Register all nodes
- Integration tests
- Manual testing in editor
Testing Plan
Unit Tests
- IF Node routes correctly on true/false
- Switch Node matches correct case
- Switch Node triggers default when no match
- For Each iterates all items
- For Each handles empty arrays
- For Each collects results
- Merge waits for all branches
- Merge combines data correctly
Integration Tests
- IF → downstream nodes execute correctly
- Switch → multiple paths work
- For Each → nested workflows work
- Merge → parallel execution converges
Success Criteria
- IF Node works in CloudRunner
- Switch Node with dynamic cases works
- For Each iterates and collects results
- Merge combines parallel paths
- All nodes appear in node picker
- Property panels work correctly
- All tests pass
Risks & Mitigations
| Risk | Mitigation |
|---|---|
| For Each performance | Limit max iterations, batch if needed |
| Merge race conditions | Use Set for tracking, atomic operations |
| Dynamic ports complexity | Follow existing patterns from BYOB |
References
- Node Patterns Guide
- LEARNINGS-NODE-CREATION
- n8n IF Node - Reference