Merge pull request #4 from johndoe6345789/copilot/fix-tech-debt-breaking-changes

Breaking: Migrate to n8n workflow schema with npm-style packages
This commit is contained in:
2026-01-10 12:53:51 +00:00
committed by GitHub
39 changed files with 2257 additions and 705 deletions

173
MIGRATION.md Normal file
View File

@@ -0,0 +1,173 @@
# Migration Guide: N8N Workflow Schema
## Overview
AutoMetabuilder has migrated from a legacy workflow format to the **n8n workflow schema**. This is a **breaking change** that provides explicit connection modeling for visual workflow canvases.
## What Changed
### Old Format (Legacy)
```json
{
"nodes": [
{
"id": "load_context",
"type": "core.load_context",
"outputs": {
"context": "sdlc_context"
}
},
{
"id": "seed_messages",
"type": "core.seed_messages",
"inputs": {
"context": "$sdlc_context"
}
}
]
}
```
### New Format (N8N)
```json
{
"name": "My Workflow",
"active": false,
"nodes": [
{
"id": "load_context",
"name": "Load Context",
"type": "core.load_context",
"typeVersion": 1,
"position": [0, 0],
"parameters": {}
},
{
"id": "seed_messages",
"name": "Seed Messages",
"type": "core.seed_messages",
"typeVersion": 1,
"position": [300, 0],
"parameters": {}
}
],
"connections": {
"Load Context": {
"main": {
"0": [
{
"node": "Seed Messages",
"type": "main",
"index": 0
}
]
}
}
}
}
```
## Key Differences
1. **Explicit Connections**: Connections are no longer implicit via variable bindings (`$varname`) but explicit in a `connections` object
2. **Node Positions**: Each node now has a `position` array `[x, y]` for canvas placement
3. **Type Versioning**: Nodes include a `typeVersion` field for schema evolution
4. **Node Names**: Nodes have both an `id` and a human-readable `name`
5. **Parameters**: Node inputs are now in a `parameters` object instead of `inputs`
6. **Workflow Metadata**: Top-level workflow has `name`, `active`, and optional `settings`
## Package Structure Changes
### Old Structure
```
workflow_packages/
├── blank.json
├── single_pass.json
└── iterative_loop.json
```
### New Structure (NPM-style)
```
packages/
├── blank/
│ ├── package.json
│ └── workflow.json
├── single_pass/
│ ├── package.json
│ └── workflow.json
└── iterative_loop/
├── package.json
└── workflow.json
```
Each package now has:
- `package.json` - Metadata (name, version, description, keywords, license)
- `workflow.json` - The n8n workflow definition
## Migration Steps
### For Existing Workflows
1. **Add required n8n fields** to each node:
- `name`: Human-readable name
- `typeVersion`: Set to `1`
- `position`: Canvas coordinates `[x, y]`
2. **Move inputs to parameters**:
- Old: `"inputs": {"messages": "$messages"}`
- New: `"parameters": {}`
3. **Build explicit connections**:
- Identify data flow from `outputs``inputs` with `$` prefixes
- Create `connections` object mapping source → targets
4. **Add workflow metadata**:
- `name`: Workflow name
- `active`: Boolean (usually `false`)
- `connections`: Connection map
### Example Converter
Use the provided converter utility:
```python
from autometabuilder.workflow.n8n_converter import convert_to_n8n
legacy_workflow = {...} # Old format
n8n_workflow = convert_to_n8n(legacy_workflow)
```
## API Changes
### Removed
- Legacy workflow format support in engine
- Variable binding resolution (`$varname`)
- Implicit connection inference
### Added
- N8N schema validation (`n8n_schema.py`)
- Explicit connection executor (`n8n_executor.py`)
- Execution order builder (`execution_order.py`)
- NPM-style package loader (`package_loader.py`)
## Error Handling
If you try to load a legacy workflow, you'll get:
```
ValueError: Only n8n workflow format is supported
```
## Benefits
1. **Visual Canvas Ready**: Positions enable drag-and-drop workflow builders
2. **Explicit Data Flow**: Clear connection visualization
3. **Schema Versioning**: `typeVersion` enables backward compatibility
4. **Standard Format**: Compatible with n8n ecosystem and tooling
5. **Better Modularity**: NPM-style packages with metadata
## References
- N8N Schema: See `ROADMAP.md` lines 84-404
- N8N Documentation: https://docs.n8n.io/workflows/
- Package Structure: `backend/autometabuilder/packages/`

View File

@@ -1,7 +1,7 @@
{
"tools_path": "tools",
"workflow_path": "workflow.json",
"workflow_packages_path": "workflow_packages",
"workflow_packages_path": "packages",
"messages": {
"en": "messages/en",
"es": "messages/es",
@@ -14,4 +14,4 @@
"settings_descriptions_path": "metadata/settings_descriptions.json",
"suggestions_path": "metadata/suggestions.json",
"workflow_plugins_path": "metadata/workflow_plugins"
}
}

View File

@@ -0,0 +1,72 @@
# Workflow Packages
This directory contains workflow packages in NPM-style format. Each package is a self-contained workflow with metadata.
## Structure
Each package is a directory containing:
### package.json
Metadata about the workflow package:
```json
{
"name": "package-name",
"version": "1.0.0",
"description": "Human-readable description",
"author": "AutoMetabuilder",
"license": "MIT",
"keywords": ["tag1", "tag2"],
"main": "workflow.json",
"metadata": {
"label": "translation.key",
"description": "translation.key",
"tags": ["category"],
"icon": "icon-name",
"category": "templates"
}
}
```
### workflow.json
The N8N workflow definition:
```json
{
"name": "Workflow Name",
"active": false,
"nodes": [...],
"connections": {...}
}
```
## Available Packages
- **blank**: Empty workflow canvas starter
- **single_pass**: Single AI request + tool execution
- **iterative_loop**: Looping AI agent with tool calls
- **contextual_iterative_loop**: Context loading + iterative loop
- **plan_execute_summarize**: Planning workflow with summary
- **testing_triangle**: Lint + unit + UI test pipeline
- **repo_scan_context**: Repository file scanning
- **game_tick_loop**: Game engine tick simulation
## Creating New Packages
1. Create a directory: `mkdir packages/my-workflow`
2. Add `package.json` with metadata
3. Add `workflow.json` with N8N schema
4. Ensure workflow has required fields:
- nodes with id, name, type, typeVersion, position
- connections mapping
- workflow name
## Loading Packages
Packages are loaded via `load_workflow_packages()` in `web/data/workflow.py`:
```python
from autometabuilder.web.data import load_workflow_packages
packages = load_workflow_packages()
```
Each package is validated and includes both metadata and workflow definition.

View File

@@ -0,0 +1,20 @@
{
"name": "blank",
"version": "1.0.0",
"description": "meta.workflow_packages.blank.description",
"author": "AutoMetabuilder",
"license": "MIT",
"keywords": [
"starter"
],
"main": "workflow.json",
"metadata": {
"label": "meta.workflow_packages.blank.label",
"description": "meta.workflow_packages.blank.description",
"tags": [
"starter"
],
"icon": "workflow",
"category": "templates"
}
}

View File

@@ -0,0 +1,6 @@
{
"name": "Blank Canvas",
"active": false,
"nodes": [],
"connections": {}
}

View File

@@ -0,0 +1,24 @@
{
"name": "contextual_iterative_loop",
"version": "1.0.0",
"description": "meta.workflow_packages.contextual_iterative_loop.description",
"author": "AutoMetabuilder",
"license": "MIT",
"keywords": [
"context",
"loop",
"map-reduce"
],
"main": "workflow.json",
"metadata": {
"label": "meta.workflow_packages.contextual_iterative_loop.label",
"description": "meta.workflow_packages.contextual_iterative_loop.description",
"tags": [
"context",
"loop",
"map-reduce"
],
"icon": "workflow",
"category": "templates"
}
}

View File

@@ -0,0 +1,177 @@
{
"name": "meta.workflow_packages.contextual_iterative_loop.label",
"active": false,
"nodes": [
{
"id": "list_files",
"name": "List Files",
"type": "tools.list_files",
"typeVersion": 1,
"position": [
0,
50
],
"parameters": {
"path": "."
}
},
{
"id": "filter_python",
"name": "Filter Python",
"type": "utils.filter_list",
"typeVersion": 1,
"position": [
300,
50
],
"parameters": {
"items": "$repo_files",
"mode": "regex",
"pattern": "\\.py$"
}
},
{
"id": "map_python",
"name": "Map Python",
"type": "utils.map_list",
"typeVersion": 1,
"position": [
600,
50
],
"parameters": {
"items": "$python_files",
"template": "PY: {item}"
}
},
{
"id": "reduce_python",
"name": "Reduce Python",
"type": "utils.reduce_list",
"typeVersion": 1,
"position": [
900,
50
],
"parameters": {
"items": "$python_lines",
"separator": "\\n"
}
},
{
"id": "seed_messages",
"name": "Seed Messages",
"type": "core.seed_messages",
"typeVersion": 1,
"position": [
1200,
50
],
"parameters": {}
},
{
"id": "append_repo_summary",
"name": "Append Repo Summary",
"type": "core.append_context_message",
"typeVersion": 1,
"position": [
1500,
50
],
"parameters": {
"messages": "$messages",
"context": "$python_summary"
}
},
{
"id": "append_user_instruction",
"name": "Append User Instruction",
"type": "core.append_user_instruction",
"typeVersion": 1,
"position": [
1800,
50
],
"parameters": {
"messages": "$messages"
}
},
{
"id": "main_loop",
"name": "Main Loop",
"type": "control.loop",
"typeVersion": 1,
"position": [
2100,
50
],
"parameters": {
"max_iterations": 5,
"stop_when": "$no_tool_calls",
"stop_on": "true"
}
}
],
"connections": {
"List Files": {
"main": {
"0": [
{
"node": "Filter Python",
"type": "main",
"index": 0
}
]
}
},
"Filter Python": {
"main": {
"0": [
{
"node": "Map Python",
"type": "main",
"index": 0
}
]
}
},
"Map Python": {
"main": {
"0": [
{
"node": "Reduce Python",
"type": "main",
"index": 0
}
]
}
},
"Append User Instruction": {
"main": {
"0": [
{
"node": "Append Repo Summary",
"type": "main",
"index": 0
},
{
"node": "Append User Instruction",
"type": "main",
"index": 0
}
]
}
},
"Reduce Python": {
"main": {
"0": [
{
"node": "Append Repo Summary",
"type": "main",
"index": 0
}
]
}
}
}
}

View File

@@ -0,0 +1,24 @@
{
"name": "game_tick_loop",
"version": "1.0.0",
"description": "meta.workflow_packages.game_tick_loop.description",
"author": "AutoMetabuilder",
"license": "MIT",
"keywords": [
"game",
"loop",
"ticks"
],
"main": "workflow.json",
"metadata": {
"label": "meta.workflow_packages.game_tick_loop.label",
"description": "meta.workflow_packages.game_tick_loop.description",
"tags": [
"game",
"loop",
"ticks"
],
"icon": "workflow",
"category": "templates"
}
}

View File

@@ -0,0 +1,113 @@
{
"name": "meta.workflow_packages.game_tick_loop.label",
"active": false,
"nodes": [
{
"id": "seed_messages",
"name": "Seed Messages",
"type": "core.seed_messages",
"typeVersion": 1,
"position": [
0,
50
],
"parameters": {}
},
{
"id": "map_ticks",
"name": "Map Ticks",
"type": "utils.map_list",
"typeVersion": 1,
"position": [
300,
50
],
"parameters": {
"items": [
"tick_start",
"tick_update",
"tick_render"
],
"template": "Tick: {item}"
}
},
{
"id": "reduce_ticks",
"name": "Reduce Ticks",
"type": "utils.reduce_list",
"typeVersion": 1,
"position": [
600,
50
],
"parameters": {
"items": "$tick_lines",
"separator": "\\n"
}
},
{
"id": "append_tick_context",
"name": "Append Tick Context",
"type": "core.append_context_message",
"typeVersion": 1,
"position": [
900,
50
],
"parameters": {
"messages": "$messages",
"context": "$tick_context"
}
},
{
"id": "main_loop",
"name": "Main Loop",
"type": "control.loop",
"typeVersion": 1,
"position": [
1200,
50
],
"parameters": {
"max_iterations": 3,
"stop_when": "$no_tool_calls",
"stop_on": "true"
}
}
],
"connections": {
"Map Ticks": {
"main": {
"0": [
{
"node": "Reduce Ticks",
"type": "main",
"index": 0
}
]
}
},
"Append Tick Context": {
"main": {
"0": [
{
"node": "Append Tick Context",
"type": "main",
"index": 0
}
]
}
},
"Reduce Ticks": {
"main": {
"0": [
{
"node": "Append Tick Context",
"type": "main",
"index": 0
}
]
}
}
}
}

View File

@@ -0,0 +1,22 @@
{
"name": "iterative_loop",
"version": "1.0.0",
"description": "meta.workflow_packages.iterative_loop.description",
"author": "AutoMetabuilder",
"license": "MIT",
"keywords": [
"loop",
"tools"
],
"main": "workflow.json",
"metadata": {
"label": "meta.workflow_packages.iterative_loop.label",
"description": "meta.workflow_packages.iterative_loop.description",
"tags": [
"loop",
"tools"
],
"icon": "workflow",
"category": "templates"
}
}

View File

@@ -0,0 +1,188 @@
{
"name": "Iterative Agent Loop",
"active": false,
"nodes": [
{
"id": "load_context",
"name": "Load Context",
"type": "core.load_context",
"typeVersion": 1,
"position": [
0,
0
],
"parameters": {}
},
{
"id": "seed_messages",
"name": "Seed Messages",
"type": "core.seed_messages",
"typeVersion": 1,
"position": [
0,
100
],
"parameters": {}
},
{
"id": "append_context",
"name": "Append Context",
"type": "core.append_context_message",
"typeVersion": 1,
"position": [
300,
50
],
"parameters": {}
},
{
"id": "append_user_instruction",
"name": "Append User Instruction",
"type": "core.append_user_instruction",
"typeVersion": 1,
"position": [
600,
50
],
"parameters": {}
},
{
"id": "main_loop",
"name": "Main Loop",
"type": "control.loop",
"typeVersion": 1,
"position": [
900,
50
],
"parameters": {
"max_iterations": 10,
"stop_when": "$no_tool_calls",
"stop_on": "true"
}
},
{
"id": "ai_request",
"name": "AI Request",
"type": "core.ai_request",
"typeVersion": 1,
"position": [
1200,
50
],
"parameters": {}
},
{
"id": "run_tool_calls",
"name": "Run Tool Calls",
"type": "core.run_tool_calls",
"typeVersion": 1,
"position": [
1500,
50
],
"parameters": {}
},
{
"id": "append_tool_results",
"name": "Append Tool Results",
"type": "core.append_tool_results",
"typeVersion": 1,
"position": [
1800,
50
],
"parameters": {}
}
],
"connections": {
"Load Context": {
"main": {
"0": [
{
"node": "Append Context",
"type": "main",
"index": 0
}
]
}
},
"Seed Messages": {
"main": {
"0": [
{
"node": "Append Context",
"type": "main",
"index": 0
}
]
}
},
"Append Context": {
"main": {
"0": [
{
"node": "Append User Instruction",
"type": "main",
"index": 0
}
]
}
},
"Append User Instruction": {
"main": {
"0": [
{
"node": "Main Loop",
"type": "main",
"index": 0
}
]
}
},
"Main Loop": {
"main": {
"0": [
{
"node": "AI Request",
"type": "main",
"index": 0
}
]
}
},
"AI Request": {
"main": {
"0": [
{
"node": "Run Tool Calls",
"type": "main",
"index": 0
}
]
}
},
"Run Tool Calls": {
"main": {
"0": [
{
"node": "Append Tool Results",
"type": "main",
"index": 0
}
]
}
},
"Append Tool Results": {
"main": {
"0": [
{
"node": "Main Loop",
"type": "main",
"index": 0
}
]
}
}
}
}

View File

@@ -0,0 +1,22 @@
{
"name": "plan_execute_summarize",
"version": "1.0.0",
"description": "meta.workflow_packages.plan_execute_summarize.description",
"author": "AutoMetabuilder",
"license": "MIT",
"keywords": [
"plan",
"summarize"
],
"main": "workflow.json",
"metadata": {
"label": "meta.workflow_packages.plan_execute_summarize.label",
"description": "meta.workflow_packages.plan_execute_summarize.description",
"tags": [
"plan",
"summarize"
],
"icon": "workflow",
"category": "templates"
}
}

View File

@@ -0,0 +1,174 @@
{
"name": "meta.workflow_packages.plan_execute_summarize.label",
"active": false,
"nodes": [
{
"id": "load_context",
"name": "Load Context",
"type": "core.load_context",
"typeVersion": 1,
"position": [
0,
50
],
"parameters": {}
},
{
"id": "seed_messages",
"name": "Seed Messages",
"type": "core.seed_messages",
"typeVersion": 1,
"position": [
300,
50
],
"parameters": {}
},
{
"id": "append_context",
"name": "Append Context",
"type": "core.append_context_message",
"typeVersion": 1,
"position": [
600,
50
],
"parameters": {
"messages": "$messages",
"context": "$sdlc_context"
}
},
{
"id": "append_user_instruction",
"name": "Append User Instruction",
"type": "core.append_user_instruction",
"typeVersion": 1,
"position": [
900,
50
],
"parameters": {
"messages": "$messages"
}
},
{
"id": "planner_request",
"name": "Planner Request",
"type": "core.ai_request",
"typeVersion": 1,
"position": [
1200,
50
],
"parameters": {
"messages": "$messages"
}
},
{
"id": "run_tool_calls",
"name": "Run Tool Calls",
"type": "core.run_tool_calls",
"typeVersion": 1,
"position": [
1500,
50
],
"parameters": {
"response": "$llm_response"
}
},
{
"id": "append_tool_results",
"name": "Append Tool Results",
"type": "core.append_tool_results",
"typeVersion": 1,
"position": [
1800,
50
],
"parameters": {
"messages": "$messages",
"tool_results": "$tool_results"
}
},
{
"id": "summary_request",
"name": "Summary Request",
"type": "core.ai_request",
"typeVersion": 1,
"position": [
2100,
50
],
"parameters": {
"messages": "$messages"
}
}
],
"connections": {
"Append Tool Results": {
"main": {
"0": [
{
"node": "Append Context",
"type": "main",
"index": 0
},
{
"node": "Append User Instruction",
"type": "main",
"index": 0
},
{
"node": "Planner Request",
"type": "main",
"index": 0
},
{
"node": "Append Tool Results",
"type": "main",
"index": 0
},
{
"node": "Summary Request",
"type": "main",
"index": 0
}
]
}
},
"Load Context": {
"main": {
"0": [
{
"node": "Append Context",
"type": "main",
"index": 0
}
]
}
},
"Planner Request": {
"main": {
"0": [
{
"node": "Run Tool Calls",
"type": "main",
"index": 0
}
]
}
},
"Run Tool Calls": {
"main": {
"0": [
{
"node": "Append Tool Results",
"type": "main",
"index": 0
}
]
}
}
}
}

View File

@@ -0,0 +1,24 @@
{
"name": "repo_scan_context",
"version": "1.0.0",
"description": "meta.workflow_packages.repo_scan_context.description",
"author": "AutoMetabuilder",
"license": "MIT",
"keywords": [
"map",
"reduce",
"context"
],
"main": "workflow.json",
"metadata": {
"label": "meta.workflow_packages.repo_scan_context.label",
"description": "meta.workflow_packages.repo_scan_context.description",
"tags": [
"map",
"reduce",
"context"
],
"icon": "workflow",
"category": "templates"
}
}

View File

@@ -0,0 +1,209 @@
{
"name": "meta.workflow_packages.repo_scan_context.label",
"active": false,
"nodes": [
{
"id": "list_files",
"name": "List Files",
"type": "tools.list_files",
"typeVersion": 1,
"position": [
0,
50
],
"parameters": {
"path": "."
}
},
{
"id": "filter_python",
"name": "Filter Python",
"type": "utils.filter_list",
"typeVersion": 1,
"position": [
300,
50
],
"parameters": {
"items": "$repo_files",
"mode": "regex",
"pattern": "\\.py$"
}
},
{
"id": "reduce_python",
"name": "Reduce Python",
"type": "utils.reduce_list",
"typeVersion": 1,
"position": [
600,
50
],
"parameters": {
"items": "$python_files",
"separator": "\\n"
}
},
{
"id": "seed_messages",
"name": "Seed Messages",
"type": "core.seed_messages",
"typeVersion": 1,
"position": [
900,
50
],
"parameters": {}
},
{
"id": "append_repo_summary",
"name": "Append Repo Summary",
"type": "core.append_context_message",
"typeVersion": 1,
"position": [
1200,
50
],
"parameters": {
"messages": "$messages",
"context": "$python_summary"
}
},
{
"id": "append_user_instruction",
"name": "Append User Instruction",
"type": "core.append_user_instruction",
"typeVersion": 1,
"position": [
1500,
50
],
"parameters": {
"messages": "$messages"
}
},
{
"id": "ai_request",
"name": "Ai Request",
"type": "core.ai_request",
"typeVersion": 1,
"position": [
1800,
50
],
"parameters": {
"messages": "$messages"
}
},
{
"id": "run_tool_calls",
"name": "Run Tool Calls",
"type": "core.run_tool_calls",
"typeVersion": 1,
"position": [
2100,
50
],
"parameters": {
"response": "$llm_response"
}
},
{
"id": "append_tool_results",
"name": "Append Tool Results",
"type": "core.append_tool_results",
"typeVersion": 1,
"position": [
2400,
50
],
"parameters": {
"messages": "$messages",
"tool_results": "$tool_results"
}
}
],
"connections": {
"List Files": {
"main": {
"0": [
{
"node": "Filter Python",
"type": "main",
"index": 0
}
]
}
},
"Filter Python": {
"main": {
"0": [
{
"node": "Reduce Python",
"type": "main",
"index": 0
}
]
}
},
"Append Tool Results": {
"main": {
"0": [
{
"node": "Append Repo Summary",
"type": "main",
"index": 0
},
{
"node": "Append User Instruction",
"type": "main",
"index": 0
},
{
"node": "Ai Request",
"type": "main",
"index": 0
},
{
"node": "Append Tool Results",
"type": "main",
"index": 0
}
]
}
},
"Reduce Python": {
"main": {
"0": [
{
"node": "Append Repo Summary",
"type": "main",
"index": 0
}
]
}
},
"Ai Request": {
"main": {
"0": [
{
"node": "Run Tool Calls",
"type": "main",
"index": 0
}
]
}
},
"Run Tool Calls": {
"main": {
"0": [
{
"node": "Append Tool Results",
"type": "main",
"index": 0
}
]
}
}
}
}

View File

@@ -0,0 +1,22 @@
{
"name": "single_pass",
"version": "1.0.0",
"description": "meta.workflow_packages.single_pass.description",
"author": "AutoMetabuilder",
"license": "MIT",
"keywords": [
"single",
"tools"
],
"main": "workflow.json",
"metadata": {
"label": "meta.workflow_packages.single_pass.label",
"description": "meta.workflow_packages.single_pass.description",
"tags": [
"single",
"tools"
],
"icon": "workflow",
"category": "templates"
}
}

View File

@@ -0,0 +1,151 @@
{
"name": "Single Pass",
"active": false,
"nodes": [
{
"id": "load_context",
"name": "Load Context",
"type": "core.load_context",
"typeVersion": 1,
"position": [
0,
0
],
"parameters": {}
},
{
"id": "seed_messages",
"name": "Seed Messages",
"type": "core.seed_messages",
"typeVersion": 1,
"position": [
0,
100
],
"parameters": {}
},
{
"id": "append_context",
"name": "Append Context",
"type": "core.append_context_message",
"typeVersion": 1,
"position": [
300,
50
],
"parameters": {}
},
{
"id": "append_user_instruction",
"name": "Append User Instruction",
"type": "core.append_user_instruction",
"typeVersion": 1,
"position": [
600,
50
],
"parameters": {}
},
{
"id": "ai_request",
"name": "AI Request",
"type": "core.ai_request",
"typeVersion": 1,
"position": [
900,
50
],
"parameters": {}
},
{
"id": "run_tool_calls",
"name": "Run Tool Calls",
"type": "core.run_tool_calls",
"typeVersion": 1,
"position": [
1200,
50
],
"parameters": {}
},
{
"id": "append_tool_results",
"name": "Append Tool Results",
"type": "core.append_tool_results",
"typeVersion": 1,
"position": [
1500,
50
],
"parameters": {}
}
],
"connections": {
"Load Context": {
"main": {
"0": [
{
"node": "Append Context",
"type": "main",
"index": 0
}
]
}
},
"Seed Messages": {
"main": {
"0": [
{
"node": "Append Context",
"type": "main",
"index": 0
}
]
}
},
"Append Context": {
"main": {
"0": [
{
"node": "Append User Instruction",
"type": "main",
"index": 0
}
]
}
},
"Append User Instruction": {
"main": {
"0": [
{
"node": "AI Request",
"type": "main",
"index": 0
}
]
}
},
"AI Request": {
"main": {
"0": [
{
"node": "Run Tool Calls",
"type": "main",
"index": 0
}
]
}
},
"Run Tool Calls": {
"main": {
"0": [
{
"node": "Append Tool Results",
"type": "main",
"index": 0
}
]
}
}
}
}

View File

@@ -0,0 +1,24 @@
{
"name": "testing_triangle",
"version": "1.0.0",
"description": "meta.workflow_packages.testing_triangle.description",
"author": "AutoMetabuilder",
"license": "MIT",
"keywords": [
"testing",
"lint",
"e2e"
],
"main": "workflow.json",
"metadata": {
"label": "meta.workflow_packages.testing_triangle.label",
"description": "meta.workflow_packages.testing_triangle.description",
"tags": [
"testing",
"lint",
"e2e"
],
"icon": "workflow",
"category": "templates"
}
}

View File

@@ -0,0 +1,147 @@
{
"name": "meta.workflow_packages.testing_triangle.label",
"active": false,
"nodes": [
{
"id": "lint",
"name": "Lint",
"type": "tools.run_lint",
"typeVersion": 1,
"position": [
0,
50
],
"parameters": {
"path": "src"
}
},
{
"id": "lint_failed",
"name": "Lint Failed",
"type": "utils.branch_condition",
"typeVersion": 1,
"position": [
300,
50
],
"parameters": {
"value": "$lint_results",
"mode": "regex",
"compare": "(FAILED|ERROR)"
}
},
{
"id": "lint_ok",
"name": "Lint Ok",
"type": "utils.not",
"typeVersion": 1,
"position": [
600,
50
],
"parameters": {
"value": "$lint_failed"
}
},
{
"id": "unit_tests",
"name": "Unit Tests",
"type": "tools.run_tests",
"typeVersion": 1,
"position": [
900,
50
],
"parameters": {
"path": "tests"
}
},
{
"id": "unit_failed",
"name": "Unit Failed",
"type": "utils.branch_condition",
"typeVersion": 1,
"position": [
1200,
50
],
"parameters": {
"value": "$unit_results",
"mode": "regex",
"compare": "(FAILED|ERROR)"
}
},
{
"id": "unit_ok",
"name": "Unit Ok",
"type": "utils.not",
"typeVersion": 1,
"position": [
1500,
50
],
"parameters": {
"value": "$unit_failed"
}
},
{
"id": "ui_tests",
"name": "Ui Tests",
"type": "tools.run_tests",
"typeVersion": 1,
"position": [
1800,
50
],
"parameters": {
"path": "tests/ui"
}
}
],
"connections": {
"Lint": {
"main": {
"0": [
{
"node": "Lint Failed",
"type": "main",
"index": 0
}
]
}
},
"Lint Failed": {
"main": {
"0": [
{
"node": "Lint Ok",
"type": "main",
"index": 0
}
]
}
},
"Unit Tests": {
"main": {
"0": [
{
"node": "Unit Failed",
"type": "main",
"index": 0
}
]
}
},
"Unit Failed": {
"main": {
"0": [
{
"node": "Unit Ok",
"type": "main",
"index": 0
}
]
}
}
}
}

View File

@@ -0,0 +1,74 @@
"""Load workflow packages from npm-style package directories."""
from __future__ import annotations
import logging
from pathlib import Path
from typing import Any, Dict, List
from .json_utils import read_json
logger = logging.getLogger(__name__)
def load_package(package_dir: Path) -> Dict[str, Any] | None:
"""Load a single workflow package."""
package_json = package_dir / "package.json"
if not package_json.exists():
logger.warning("Package %s missing package.json", package_dir.name)
return None
# Read package.json
pkg_data = read_json(package_json)
if not isinstance(pkg_data, dict):
logger.warning("Invalid package.json in %s", package_dir.name)
return None
# Read workflow file
workflow_file = pkg_data.get("main", "workflow.json")
workflow_path = package_dir / workflow_file
if not workflow_path.exists():
logger.warning("Workflow file %s not found in %s", workflow_file, package_dir.name)
return None
workflow_data = read_json(workflow_path)
if not isinstance(workflow_data, dict):
logger.warning("Invalid workflow in %s", package_dir.name)
return None
# Combine package metadata with workflow
metadata = pkg_data.get("metadata", {})
return {
"id": pkg_data.get("name", package_dir.name),
"name": pkg_data.get("name", package_dir.name),
"version": pkg_data.get("version", "1.0.0"),
"description": pkg_data.get("description", ""),
"author": pkg_data.get("author", ""),
"license": pkg_data.get("license", ""),
"keywords": pkg_data.get("keywords", []),
"label": metadata.get("label", package_dir.name),
"tags": metadata.get("tags", []),
"icon": metadata.get("icon", "workflow"),
"category": metadata.get("category", "templates"),
"workflow": workflow_data,
}
def load_all_packages(packages_dir: Path) -> List[Dict[str, Any]]:
"""Load all workflow packages from directory."""
if not packages_dir.exists():
logger.warning("Packages directory not found: %s", packages_dir)
return []
packages = []
for item in sorted(packages_dir.iterdir()):
if not item.is_dir():
continue
package = load_package(item)
if package:
packages.append(package)
logger.debug("Loaded %d workflow packages", len(packages))
return packages

View File

@@ -5,6 +5,7 @@ from typing import Any, Iterable
from .json_utils import read_json
from .metadata import load_metadata
from .package_loader import load_all_packages
from .paths import PACKAGE_ROOT
@@ -26,26 +27,13 @@ def write_workflow(content: str) -> None:
def get_workflow_packages_dir() -> Path:
metadata = load_metadata()
packages_name = metadata.get("workflow_packages_path", "workflow_packages")
packages_name = metadata.get("workflow_packages_path", "packages")
return PACKAGE_ROOT / packages_name
def load_workflow_packages() -> list[dict[str, Any]]:
packages_dir = get_workflow_packages_dir()
if not packages_dir.exists():
return []
packages: list[dict[str, Any]] = []
for file in sorted(packages_dir.iterdir()):
if file.suffix != ".json":
continue
data = read_json(file)
if not isinstance(data, dict):
continue
pkg_id = data.get("id") or file.stem
data["id"] = pkg_id
data.setdefault("workflow", {"nodes": []})
packages.append(data)
return packages
return load_all_packages(packages_dir)
def summarize_workflow_packages(packages: Iterable[dict[str, Any]]) -> list[dict[str, Any]]:
@@ -54,9 +42,12 @@ def summarize_workflow_packages(packages: Iterable[dict[str, Any]]) -> list[dict
summary.append(
{
"id": pkg["id"],
"name": pkg.get("name", pkg["id"]),
"label": pkg.get("label") or pkg["id"],
"description": pkg.get("description", ""),
"tags": pkg.get("tags", []),
"version": pkg.get("version", "1.0.0"),
"category": pkg.get("category", "templates"),
}
)
return summary

View File

@@ -1,4 +1,4 @@
"""Build a node/edge view of the declarative workflow for visualization."""
"""Build a node/edge view of n8n workflows for visualization."""
from __future__ import annotations
import json
@@ -13,64 +13,74 @@ logger = logging.getLogger(__name__)
def _parse_workflow_definition() -> Dict[str, Any]:
payload = get_workflow_content()
if not payload:
return {"nodes": []}
return {"name": "Empty", "nodes": [], "connections": {}}
try:
parsed = json.loads(payload)
except json.JSONDecodeError as exc:
logger.warning("Invalid workflow JSON: %s", exc)
return {"nodes": []}
return parsed if isinstance(parsed, dict) else {"nodes": []}
return {"name": "Invalid", "nodes": [], "connections": {}}
return parsed if isinstance(parsed, dict) else {"name": "Invalid", "nodes": [], "connections": {}}
def _gather_nodes(nodes: Iterable[Dict[str, Any]], plugin_map: Dict[str, Any], parent_id: str | None = None, collected: List[Dict[str, Any]] | None = None) -> List[Dict[str, Any]]:
collected = collected or []
def _gather_n8n_nodes(
nodes: Iterable[Dict[str, Any]],
plugin_map: Dict[str, Any]
) -> List[Dict[str, Any]]:
"""Extract nodes from n8n format."""
collected = []
for node in nodes:
node_id = node.get("id") or f"node-{len(collected)}"
node_id = node.get("id", node.get("name", f"node-{len(collected)}"))
node_type = node.get("type", "unknown")
metadata = plugin_map.get(node_type, {})
node_summary: Dict[str, Any] = {
collected.append({
"id": node_id,
"name": node.get("name", node_id),
"type": node_type,
"label_key": metadata.get("label"),
"parent": parent_id,
"inputs": node.get("inputs", {}),
"outputs": node.get("outputs", {}),
}
collected.append(node_summary)
body = node.get("body")
if isinstance(body, list):
_gather_nodes(body, plugin_map, parent_id=node_id, collected=collected)
"parent": None,
"position": node.get("position", [0, 0]),
})
return collected
def _build_edges(nodes: Iterable[Dict[str, Any]]) -> List[Dict[str, str]]:
producers: Dict[str, str] = {}
for node in nodes:
outputs = node.get("outputs", {})
for value in outputs.values():
if isinstance(value, str):
if value in producers:
logger.debug("Variable %s already produced by %s; overwriting with %s", value, producers[value], node["id"])
producers[value] = node["id"]
edges: List[Dict[str, str]] = []
for node in nodes:
inputs = node.get("inputs", {})
for port, value in inputs.items():
if isinstance(value, str) and value.startswith("$"):
variable = value[1:]
source = producers.get(variable)
if source:
edges.append({"from": source, "to": node["id"], "var": variable, "port": port})
else:
logger.debug("No producer found for %s referenced by %s.%s", variable, node["id"], port)
def _build_n8n_edges(
connections: Dict[str, Any],
nodes: List[Dict[str, Any]]
) -> List[Dict[str, str]]:
"""Build edges from n8n connections."""
# Build name to ID mapping
name_to_id = {node["name"]: node["id"] for node in nodes}
edges = []
for source_name, outputs in connections.items():
source_id = name_to_id.get(source_name, source_name)
for output_type, indices in outputs.items():
for index, targets in indices.items():
for target in targets:
target_name = target["node"]
target_id = name_to_id.get(target_name, target_name)
edges.append({
"from": source_id,
"to": target_id,
"type": target.get("type", "main"),
"output_index": index,
"input_index": target.get("index", 0),
})
return edges
def build_workflow_graph() -> Dict[str, Any]:
"""Build workflow graph from n8n format (breaking change: legacy format removed)."""
definition = _parse_workflow_definition()
plugin_map = load_metadata().get("workflow_plugins", {})
nodes = _gather_nodes(definition.get("nodes", []), plugin_map)
edges = _build_edges(nodes)
# Only support n8n format now
nodes = _gather_n8n_nodes(definition.get("nodes", []), plugin_map)
edges = _build_n8n_edges(definition.get("connections", {}), nodes)
logger.debug("Built workflow graph with %d nodes and %d edges", len(nodes), len(edges))
return {
"nodes": nodes,

View File

@@ -1,84 +1,116 @@
{
"name": "Default Workflow",
"active": false,
"nodes": [
{
"id": "load_context",
"name": "Load Context",
"type": "core.load_context",
"outputs": {
"context": "sdlc_context"
}
"typeVersion": 1,
"position": [0, 0],
"parameters": {}
},
{
"id": "seed_messages",
"name": "Seed Messages",
"type": "core.seed_messages",
"outputs": {
"messages": "messages"
}
"typeVersion": 1,
"position": [0, 100],
"parameters": {}
},
{
"id": "append_context",
"name": "Append Context",
"type": "core.append_context_message",
"inputs": {
"messages": "$messages",
"context": "$sdlc_context"
},
"outputs": {
"messages": "messages"
}
"typeVersion": 1,
"position": [300, 50],
"parameters": {}
},
{
"id": "append_user_instruction",
"name": "Append User Instruction",
"type": "core.append_user_instruction",
"inputs": {
"messages": "$messages"
},
"outputs": {
"messages": "messages"
}
"typeVersion": 1,
"position": [600, 50],
"parameters": {}
},
{
"id": "main_loop",
"name": "Main Loop",
"type": "control.loop",
"inputs": {
"typeVersion": 1,
"position": [900, 50],
"parameters": {
"max_iterations": 10,
"stop_when": "$no_tool_calls",
"stop_on": "true"
},
"body": [
{
"id": "ai_request",
"type": "core.ai_request",
"inputs": {
"messages": "$messages"
},
"outputs": {
"response": "llm_response",
"has_tool_calls": "has_tool_calls",
"tool_calls_count": "tool_calls_count"
}
},
{
"id": "run_tool_calls",
"type": "core.run_tool_calls",
"inputs": {
"response": "$llm_response"
},
"outputs": {
"tool_results": "tool_results",
"no_tool_calls": "no_tool_calls"
}
},
{
"id": "append_tool_results",
"type": "core.append_tool_results",
"inputs": {
"messages": "$messages",
"tool_results": "$tool_results"
},
"outputs": {
"messages": "messages"
}
}
]
}
},
{
"id": "ai_request",
"name": "AI Request",
"type": "core.ai_request",
"typeVersion": 1,
"position": [1200, 50],
"parameters": {}
},
{
"id": "run_tool_calls",
"name": "Run Tool Calls",
"type": "core.run_tool_calls",
"typeVersion": 1,
"position": [1500, 50],
"parameters": {}
},
{
"id": "append_tool_results",
"name": "Append Tool Results",
"type": "core.append_tool_results",
"typeVersion": 1,
"position": [1800, 50],
"parameters": {}
}
]
],
"connections": {
"Load Context": {
"main": {
"0": [{"node": "Append Context", "type": "main", "index": 0}]
}
},
"Seed Messages": {
"main": {
"0": [{"node": "Append Context", "type": "main", "index": 0}]
}
},
"Append Context": {
"main": {
"0": [{"node": "Append User Instruction", "type": "main", "index": 0}]
}
},
"Append User Instruction": {
"main": {
"0": [{"node": "Main Loop", "type": "main", "index": 0}]
}
},
"Main Loop": {
"main": {
"0": [{"node": "AI Request", "type": "main", "index": 0}]
}
},
"AI Request": {
"main": {
"0": [{"node": "Run Tool Calls", "type": "main", "index": 0}]
}
},
"Run Tool Calls": {
"main": {
"0": [{"node": "Append Tool Results", "type": "main", "index": 0}]
}
},
"Append Tool Results": {
"main": {
"0": [{"node": "Main Loop", "type": "main", "index": 0}]
}
}
}
}

View File

@@ -1,17 +1,31 @@
"""Workflow engine runner."""
"""Workflow engine runner for n8n format."""
from .workflow_adapter import WorkflowAdapter, is_n8n_workflow
class WorkflowEngine:
"""Run workflow configs through a node executor."""
def __init__(self, workflow_config, node_executor, logger):
"""Run n8n workflow configs (breaking change: legacy format removed)."""
def __init__(self, workflow_config, node_executor, logger, runtime=None, plugin_registry=None):
self.workflow_config = workflow_config or {}
self.node_executor = node_executor
self.logger = logger
self.runtime = runtime
self.plugin_registry = plugin_registry
# Create adapter if we have runtime and plugin registry
if runtime and plugin_registry:
self.adapter = WorkflowAdapter(node_executor, runtime, plugin_registry)
else:
self.adapter = None
def execute(self):
"""Execute the workflow config."""
nodes = self.workflow_config.get("nodes")
if not isinstance(nodes, list):
self.logger.error("Workflow config missing nodes list.")
return
self.node_executor.execute_nodes(nodes)
"""Execute the n8n workflow config."""
# Enforce n8n format only
if not is_n8n_workflow(self.workflow_config):
self.logger.error("Legacy workflow format is no longer supported. Please migrate to n8n schema.")
raise ValueError("Only n8n workflow format is supported")
if self.adapter:
self.adapter.execute(self.workflow_config)
else:
self.logger.error("Workflow engine requires runtime and plugin_registry for n8n execution")
raise RuntimeError("Cannot execute n8n workflow without runtime and plugin_registry")

View File

@@ -0,0 +1,45 @@
"""Build execution order for n8n workflows."""
from __future__ import annotations
from typing import Any, Dict, List, Set
def build_execution_order(
nodes: List[Dict[str, Any]],
connections: Dict[str, Any]
) -> List[str]:
"""Build topological execution order from connections."""
node_names = {node["name"] for node in nodes}
has_inputs = _find_nodes_with_inputs(connections)
# Start with nodes that have no inputs
order = [name for name in node_names if name not in has_inputs]
# Add remaining nodes (simplified BFS)
remaining = node_names - set(order)
order.extend(_add_remaining_nodes(remaining))
return order
def _find_nodes_with_inputs(connections: Dict[str, Any]) -> Set[str]:
"""Find all nodes that have incoming connections."""
has_inputs = set()
for source_name, outputs in connections.items():
for output_type, indices in outputs.items():
for targets in indices.values():
for target in targets:
has_inputs.add(target["node"])
return has_inputs
def _add_remaining_nodes(remaining: Set[str]) -> List[str]:
"""Add remaining nodes in order."""
order = []
while remaining:
name = next(iter(remaining))
order.append(name)
remaining.remove(name)
return order

View File

@@ -0,0 +1,110 @@
"""Convert legacy workflows to n8n schema."""
from __future__ import annotations
import logging
from typing import Any, Dict, List
from uuid import uuid4
logger = logging.getLogger(__name__)
def _generate_node_id() -> str:
"""Generate unique node ID."""
return str(uuid4())
def _calculate_position(index: int, parent_level: int = 0) -> List[float]:
"""Calculate node position on canvas."""
x = parent_level * 300.0
y = index * 100.0
return [x, y]
def _convert_node(
node: Dict[str, Any],
index: int,
parent_level: int = 0
) -> Dict[str, Any]:
"""Convert legacy node to n8n format."""
node_id = node.get("id", f"node-{_generate_node_id()}")
node_type = node.get("type", "unknown")
n8n_node: Dict[str, Any] = {
"id": node_id,
"name": node.get("name", node_id),
"type": node_type,
"typeVersion": 1,
"position": _calculate_position(index, parent_level),
"parameters": node.get("inputs", {}),
}
if node.get("disabled"):
n8n_node["disabled"] = True
if node.get("notes"):
n8n_node["notes"] = node["notes"]
return n8n_node
def _build_connections(
nodes: List[Dict[str, Any]],
legacy_nodes: List[Dict[str, Any]]
) -> Dict[str, Any]:
"""Build n8n connections from variable bindings."""
connections: Dict[str, Any] = {}
producers: Dict[str, str] = {}
# Map variable names to producer nodes
for i, legacy_node in enumerate(legacy_nodes):
outputs = legacy_node.get("outputs", {})
node_name = nodes[i]["name"]
for var_name in outputs.values():
if isinstance(var_name, str):
producers[var_name] = node_name
# Build connections from inputs
for i, legacy_node in enumerate(legacy_nodes):
inputs = legacy_node.get("inputs", {})
target_name = nodes[i]["name"]
for port, value in inputs.items():
if isinstance(value, str) and value.startswith("$"):
var_name = value[1:]
source_name = producers.get(var_name)
if source_name:
if source_name not in connections:
connections[source_name] = {"main": {}}
if "0" not in connections[source_name]["main"]:
connections[source_name]["main"]["0"] = []
connections[source_name]["main"]["0"].append({
"node": target_name,
"type": "main",
"index": 0
})
return connections
def convert_to_n8n(legacy_workflow: Dict[str, Any]) -> Dict[str, Any]:
"""Convert legacy workflow to n8n schema."""
legacy_nodes = legacy_workflow.get("nodes", [])
n8n_nodes = [
_convert_node(node, i)
for i, node in enumerate(legacy_nodes)
]
connections = _build_connections(n8n_nodes, legacy_nodes)
return {
"id": legacy_workflow.get("id", _generate_node_id()),
"name": legacy_workflow.get("name", "Workflow"),
"active": legacy_workflow.get("active", False),
"nodes": n8n_nodes,
"connections": connections,
"settings": legacy_workflow.get("settings", {}),
"tags": legacy_workflow.get("tags", []),
}

View File

@@ -0,0 +1,70 @@
"""Execute n8n-style workflows with explicit connections."""
from __future__ import annotations
import logging
from typing import Any, Dict, List
from .execution_order import build_execution_order
logger = logging.getLogger(__name__)
class N8NExecutor:
"""Execute n8n-style workflows."""
def __init__(self, runtime, plugin_registry):
self.runtime = runtime
self.plugin_registry = plugin_registry
def execute(self, workflow: Dict[str, Any]) -> None:
"""Execute n8n workflow."""
nodes = workflow.get("nodes", [])
connections = workflow.get("connections", {})
if not nodes:
logger.warning("No nodes in workflow")
return
# Build execution order from connections
execution_order = build_execution_order(nodes, connections)
# Execute nodes in order
for node_name in execution_order:
node = self._find_node_by_name(nodes, node_name)
if node:
self._execute_node(node)
def _find_node_by_name(self, nodes: List[Dict], name: str) -> Dict | None:
"""Find node by name."""
for node in nodes:
if node.get("name") == name:
return node
return None
def _execute_node(self, node: Dict[str, Any]) -> Any:
"""Execute single node."""
node_type = node.get("type")
node_name = node.get("name", node.get("id"))
if node.get("disabled"):
logger.debug("Node %s is disabled, skipping", node_name)
return None
if node_type == "control.loop":
return self._execute_loop(node)
plugin = self.plugin_registry.get(node_type)
if not plugin:
logger.error("Unknown node type: %s", node_type)
return None
inputs = node.get("parameters", {})
logger.debug("Executing node %s (%s)", node_name, node_type)
result = plugin(self.runtime, inputs)
return result
def _execute_loop(self, node: Dict[str, Any]) -> Any:
"""Execute loop node (placeholder)."""
logger.debug("Loop execution not yet implemented in n8n executor")
return None

View File

@@ -0,0 +1,72 @@
"""N8N workflow schema types and validation."""
from __future__ import annotations
from typing import Any, Dict, List, Literal, Optional, Union
class N8NPosition:
"""Canvas position [x, y]."""
@staticmethod
def validate(value: Any) -> bool:
return (
isinstance(value, list) and
len(value) == 2 and
all(isinstance(v, (int, float)) for v in value)
)
class N8NConnectionTarget:
"""Connection target specification."""
@staticmethod
def validate(value: Any) -> bool:
if not isinstance(value, dict):
return False
return (
"node" in value and isinstance(value["node"], str) and
"type" in value and isinstance(value["type"], str) and
"index" in value and isinstance(value["index"], int) and value["index"] >= 0
)
class N8NNode:
"""N8N workflow node specification."""
@staticmethod
def validate(value: Any) -> bool:
if not isinstance(value, dict):
return False
required = ["id", "name", "type", "typeVersion", "position"]
if not all(key in value for key in required):
return False
if not isinstance(value["id"], str) or not value["id"]:
return False
if not isinstance(value["name"], str) or not value["name"]:
return False
if not isinstance(value["type"], str) or not value["type"]:
return False
if not isinstance(value["typeVersion"], (int, float)) or value["typeVersion"] < 1:
return False
if not N8NPosition.validate(value["position"]):
return False
return True
class N8NWorkflow:
"""N8N workflow specification."""
@staticmethod
def validate(value: Any) -> bool:
if not isinstance(value, dict):
return False
required = ["name", "nodes", "connections"]
if not all(key in value for key in required):
return False
if not isinstance(value["name"], str) or not value["name"]:
return False
if not isinstance(value["nodes"], list) or len(value["nodes"]) < 1:
return False
if not isinstance(value["connections"], dict):
return False
return all(N8NNode.validate(node) for node in value["nodes"])

View File

@@ -0,0 +1,48 @@
"""N8N workflow format handler."""
from __future__ import annotations
import logging
from typing import Any, Dict
from .n8n_executor import N8NExecutor
logger = logging.getLogger(__name__)
def is_n8n_workflow(workflow: Dict[str, Any]) -> bool:
"""Check if workflow uses n8n schema."""
if not isinstance(workflow, dict):
return False
# N8N workflows must have explicit connections
has_connections = "connections" in workflow
nodes = workflow.get("nodes", [])
if not nodes:
return has_connections
# Check if nodes have n8n properties
first_node = nodes[0] if isinstance(nodes, list) and nodes else {}
has_position = "position" in first_node
has_type_version = "typeVersion" in first_node
has_name = "name" in first_node
return has_connections and (has_position or has_type_version or has_name)
class WorkflowAdapter:
"""Execute n8n workflows (breaking change: legacy format no longer supported)."""
def __init__(self, node_executor, runtime, plugin_registry):
self.runtime = runtime
self.plugin_registry = plugin_registry
self.n8n_executor = N8NExecutor(runtime, plugin_registry)
def execute(self, workflow: Dict[str, Any]) -> None:
"""Execute n8n workflow."""
if not is_n8n_workflow(workflow):
logger.error("Legacy workflow format is no longer supported. Please migrate to n8n schema.")
raise ValueError("Only n8n workflow format is supported")
logger.debug("Executing n8n workflow")
self.n8n_executor.execute(workflow)

View File

@@ -1,11 +0,0 @@
{
"id": "blank",
"label": "meta.workflow_packages.blank.label",
"description": "meta.workflow_packages.blank.description",
"tags": [
"starter"
],
"workflow": {
"nodes": []
}
}

View File

@@ -1,80 +0,0 @@
{
"id": "contextual_iterative_loop",
"label": "meta.workflow_packages.contextual_iterative_loop.label",
"description": "meta.workflow_packages.contextual_iterative_loop.description",
"tags": ["context", "loop", "map-reduce"],
"workflow": {
"nodes": [
{
"id": "list_files",
"type": "tools.list_files",
"inputs": {"path": "."},
"outputs": {"files": "repo_files"}
},
{
"id": "filter_python",
"type": "utils.filter_list",
"inputs": {"items": "$repo_files", "mode": "regex", "pattern": "\\.py$"},
"outputs": {"items": "python_files"}
},
{
"id": "map_python",
"type": "utils.map_list",
"inputs": {"items": "$python_files", "template": "PY: {item}"},
"outputs": {"items": "python_lines"}
},
{
"id": "reduce_python",
"type": "utils.reduce_list",
"inputs": {"items": "$python_lines", "separator": "\\n"},
"outputs": {"result": "python_summary"}
},
{
"id": "seed_messages",
"type": "core.seed_messages",
"outputs": {"messages": "messages"}
},
{
"id": "append_repo_summary",
"type": "core.append_context_message",
"inputs": {"messages": "$messages", "context": "$python_summary"},
"outputs": {"messages": "messages"}
},
{
"id": "append_user_instruction",
"type": "core.append_user_instruction",
"inputs": {"messages": "$messages"},
"outputs": {"messages": "messages"}
},
{
"id": "main_loop",
"type": "control.loop",
"inputs": {"max_iterations": 5, "stop_when": "$no_tool_calls", "stop_on": "true"},
"body": [
{
"id": "ai_request",
"type": "core.ai_request",
"inputs": {"messages": "$messages"},
"outputs": {
"response": "llm_response",
"has_tool_calls": "has_tool_calls",
"tool_calls_count": "tool_calls_count"
}
},
{
"id": "run_tool_calls",
"type": "core.run_tool_calls",
"inputs": {"response": "$llm_response"},
"outputs": {"tool_results": "tool_results", "no_tool_calls": "no_tool_calls"}
},
{
"id": "append_tool_results",
"type": "core.append_tool_results",
"inputs": {"messages": "$messages", "tool_results": "$tool_results"},
"outputs": {"messages": "messages"}
}
]
}
]
}
}

View File

@@ -1,65 +0,0 @@
{
"id": "game_tick_loop",
"label": "meta.workflow_packages.game_tick_loop.label",
"description": "meta.workflow_packages.game_tick_loop.description",
"tags": ["game", "loop", "ticks"],
"workflow": {
"nodes": [
{
"id": "seed_messages",
"type": "core.seed_messages",
"outputs": {"messages": "messages"}
},
{
"id": "map_ticks",
"type": "utils.map_list",
"inputs": {
"items": ["tick_start", "tick_update", "tick_render"],
"template": "Tick: {item}"
},
"outputs": {"items": "tick_lines"}
},
{
"id": "reduce_ticks",
"type": "utils.reduce_list",
"inputs": {"items": "$tick_lines", "separator": "\\n"},
"outputs": {"result": "tick_context"}
},
{
"id": "append_tick_context",
"type": "core.append_context_message",
"inputs": {"messages": "$messages", "context": "$tick_context"},
"outputs": {"messages": "messages"}
},
{
"id": "main_loop",
"type": "control.loop",
"inputs": {"max_iterations": 3, "stop_when": "$no_tool_calls", "stop_on": "true"},
"body": [
{
"id": "ai_request",
"type": "core.ai_request",
"inputs": {"messages": "$messages"},
"outputs": {
"response": "llm_response",
"has_tool_calls": "has_tool_calls",
"tool_calls_count": "tool_calls_count"
}
},
{
"id": "run_tool_calls",
"type": "core.run_tool_calls",
"inputs": {"response": "$llm_response"},
"outputs": {"tool_results": "tool_results", "no_tool_calls": "no_tool_calls"}
},
{
"id": "append_tool_results",
"type": "core.append_tool_results",
"inputs": {"messages": "$messages", "tool_results": "$tool_results"},
"outputs": {"messages": "messages"}
}
]
}
]
}
}

View File

@@ -1,93 +0,0 @@
{
"id": "iterative_loop",
"label": "meta.workflow_packages.iterative_loop.label",
"description": "meta.workflow_packages.iterative_loop.description",
"tags": [
"loop",
"tools"
],
"workflow": {
"nodes": [
{
"id": "load_context",
"type": "core.load_context",
"outputs": {
"context": "sdlc_context"
}
},
{
"id": "seed_messages",
"type": "core.seed_messages",
"outputs": {
"messages": "messages"
}
},
{
"id": "append_context",
"type": "core.append_context_message",
"inputs": {
"messages": "$messages",
"context": "$sdlc_context"
},
"outputs": {
"messages": "messages"
}
},
{
"id": "append_user_instruction",
"type": "core.append_user_instruction",
"inputs": {
"messages": "$messages"
},
"outputs": {
"messages": "messages"
}
},
{
"id": "main_loop",
"type": "control.loop",
"inputs": {
"max_iterations": 10,
"stop_when": "$no_tool_calls",
"stop_on": "true"
},
"body": [
{
"id": "ai_request",
"type": "core.ai_request",
"inputs": {
"messages": "$messages"
},
"outputs": {
"response": "llm_response",
"has_tool_calls": "has_tool_calls",
"tool_calls_count": "tool_calls_count"
}
},
{
"id": "run_tool_calls",
"type": "core.run_tool_calls",
"inputs": {
"response": "$llm_response"
},
"outputs": {
"tool_results": "tool_results",
"no_tool_calls": "no_tool_calls"
}
},
{
"id": "append_tool_results",
"type": "core.append_tool_results",
"inputs": {
"messages": "$messages",
"tool_results": "$tool_results"
},
"outputs": {
"messages": "messages"
}
}
]
}
]
}
}

View File

@@ -1,94 +0,0 @@
{
"id": "plan_execute_summarize",
"label": "meta.workflow_packages.plan_execute_summarize.label",
"description": "meta.workflow_packages.plan_execute_summarize.description",
"tags": [
"plan",
"summarize"
],
"workflow": {
"nodes": [
{
"id": "load_context",
"type": "core.load_context",
"outputs": {
"context": "sdlc_context"
}
},
{
"id": "seed_messages",
"type": "core.seed_messages",
"outputs": {
"messages": "messages"
}
},
{
"id": "append_context",
"type": "core.append_context_message",
"inputs": {
"messages": "$messages",
"context": "$sdlc_context"
},
"outputs": {
"messages": "messages"
}
},
{
"id": "append_user_instruction",
"type": "core.append_user_instruction",
"inputs": {
"messages": "$messages"
},
"outputs": {
"messages": "messages"
}
},
{
"id": "planner_request",
"type": "core.ai_request",
"inputs": {
"messages": "$messages"
},
"outputs": {
"response": "llm_response",
"has_tool_calls": "has_tool_calls",
"tool_calls_count": "tool_calls_count"
}
},
{
"id": "run_tool_calls",
"type": "core.run_tool_calls",
"inputs": {
"response": "$llm_response"
},
"outputs": {
"tool_results": "tool_results",
"no_tool_calls": "no_tool_calls"
}
},
{
"id": "append_tool_results",
"type": "core.append_tool_results",
"inputs": {
"messages": "$messages",
"tool_results": "$tool_results"
},
"outputs": {
"messages": "messages"
}
},
{
"id": "summary_request",
"type": "core.ai_request",
"inputs": {
"messages": "$messages"
},
"outputs": {
"response": "final_response",
"has_tool_calls": "final_has_tool_calls",
"tool_calls_count": "final_tool_calls_count"
}
}
]
}
}

View File

@@ -1,67 +0,0 @@
{
"id": "repo_scan_context",
"label": "meta.workflow_packages.repo_scan_context.label",
"description": "meta.workflow_packages.repo_scan_context.description",
"tags": ["map", "reduce", "context"],
"workflow": {
"nodes": [
{
"id": "list_files",
"type": "tools.list_files",
"inputs": {"path": "."},
"outputs": {"files": "repo_files"}
},
{
"id": "filter_python",
"type": "utils.filter_list",
"inputs": {"items": "$repo_files", "mode": "regex", "pattern": "\\.py$"},
"outputs": {"items": "python_files"}
},
{
"id": "reduce_python",
"type": "utils.reduce_list",
"inputs": {"items": "$python_files", "separator": "\\n"},
"outputs": {"result": "python_summary"}
},
{
"id": "seed_messages",
"type": "core.seed_messages",
"outputs": {"messages": "messages"}
},
{
"id": "append_repo_summary",
"type": "core.append_context_message",
"inputs": {"messages": "$messages", "context": "$python_summary"},
"outputs": {"messages": "messages"}
},
{
"id": "append_user_instruction",
"type": "core.append_user_instruction",
"inputs": {"messages": "$messages"},
"outputs": {"messages": "messages"}
},
{
"id": "ai_request",
"type": "core.ai_request",
"inputs": {"messages": "$messages"},
"outputs": {
"response": "llm_response",
"has_tool_calls": "has_tool_calls",
"tool_calls_count": "tool_calls_count"
}
},
{
"id": "run_tool_calls",
"type": "core.run_tool_calls",
"inputs": {"response": "$llm_response"},
"outputs": {"tool_results": "tool_results", "no_tool_calls": "no_tool_calls"}
},
{
"id": "append_tool_results",
"type": "core.append_tool_results",
"inputs": {"messages": "$messages", "tool_results": "$tool_results"},
"outputs": {"messages": "messages"}
}
]
}
}

View File

@@ -1,82 +0,0 @@
{
"id": "single_pass",
"label": "meta.workflow_packages.single_pass.label",
"description": "meta.workflow_packages.single_pass.description",
"tags": [
"single",
"tools"
],
"workflow": {
"nodes": [
{
"id": "load_context",
"type": "core.load_context",
"outputs": {
"context": "sdlc_context"
}
},
{
"id": "seed_messages",
"type": "core.seed_messages",
"outputs": {
"messages": "messages"
}
},
{
"id": "append_context",
"type": "core.append_context_message",
"inputs": {
"messages": "$messages",
"context": "$sdlc_context"
},
"outputs": {
"messages": "messages"
}
},
{
"id": "append_user_instruction",
"type": "core.append_user_instruction",
"inputs": {
"messages": "$messages"
},
"outputs": {
"messages": "messages"
}
},
{
"id": "ai_request",
"type": "core.ai_request",
"inputs": {
"messages": "$messages"
},
"outputs": {
"response": "llm_response",
"has_tool_calls": "has_tool_calls",
"tool_calls_count": "tool_calls_count"
}
},
{
"id": "run_tool_calls",
"type": "core.run_tool_calls",
"inputs": {
"response": "$llm_response"
},
"outputs": {
"tool_results": "tool_results",
"no_tool_calls": "no_tool_calls"
}
},
{
"id": "append_tool_results",
"type": "core.append_tool_results",
"inputs": {
"messages": "$messages",
"tool_results": "$tool_results"
},
"outputs": {
"messages": "messages"
}
}
]
}
}

View File

@@ -1,88 +0,0 @@
{
"id": "testing_triangle",
"label": "meta.workflow_packages.testing_triangle.label",
"description": "meta.workflow_packages.testing_triangle.description",
"tags": ["testing", "lint", "e2e"],
"workflow": {
"nodes": [
{
"id": "lint",
"type": "tools.run_lint",
"inputs": {
"path": "src"
},
"outputs": {
"results": "lint_results"
}
},
{
"id": "lint_failed",
"type": "utils.branch_condition",
"inputs": {
"value": "$lint_results",
"mode": "regex",
"compare": "(FAILED|ERROR)"
},
"outputs": {
"result": "lint_failed"
}
},
{
"id": "lint_ok",
"type": "utils.not",
"inputs": {
"value": "$lint_failed"
},
"outputs": {
"result": "lint_ok"
}
},
{
"id": "unit_tests",
"type": "tools.run_tests",
"when": "$lint_ok",
"inputs": {
"path": "tests"
},
"outputs": {
"results": "unit_results"
}
},
{
"id": "unit_failed",
"type": "utils.branch_condition",
"when": "$lint_ok",
"inputs": {
"value": "$unit_results",
"mode": "regex",
"compare": "(FAILED|ERROR)"
},
"outputs": {
"result": "unit_failed"
}
},
{
"id": "unit_ok",
"type": "utils.not",
"when": "$lint_ok",
"inputs": {
"value": "$unit_failed"
},
"outputs": {
"result": "unit_ok"
}
},
{
"id": "ui_tests",
"type": "tools.run_tests",
"when": "$unit_ok",
"inputs": {
"path": "tests/ui"
},
"outputs": {
"results": "ui_results"
}
}
]
}
}

View File

@@ -0,0 +1,73 @@
"""Tests for n8n workflow schema validation."""
import pytest
from autometabuilder.workflow.n8n_schema import N8NNode, N8NPosition, N8NWorkflow
def test_n8n_position_validation():
"""Test position validation."""
assert N8NPosition.validate([0, 0])
assert N8NPosition.validate([100.5, 200.5])
assert not N8NPosition.validate([0])
assert not N8NPosition.validate([0, 0, 0])
assert not N8NPosition.validate("invalid")
assert not N8NPosition.validate(None)
def test_n8n_node_validation():
"""Test node validation."""
valid_node = {
"id": "node-1",
"name": "Test Node",
"type": "core.test",
"typeVersion": 1,
"position": [0, 0],
"parameters": {}
}
assert N8NNode.validate(valid_node)
# Missing required fields
assert not N8NNode.validate({})
assert not N8NNode.validate({"id": "node-1"})
# Invalid typeVersion
invalid_node = valid_node.copy()
invalid_node["typeVersion"] = 0
assert not N8NNode.validate(invalid_node)
# Invalid position
invalid_node = valid_node.copy()
invalid_node["position"] = [0]
assert not N8NNode.validate(invalid_node)
def test_n8n_workflow_validation():
"""Test workflow validation."""
valid_workflow = {
"name": "Test Workflow",
"nodes": [
{
"id": "node-1",
"name": "Node 1",
"type": "core.test",
"typeVersion": 1,
"position": [0, 0]
}
],
"connections": {}
}
assert N8NWorkflow.validate(valid_workflow)
# Missing required fields
assert not N8NWorkflow.validate({})
assert not N8NWorkflow.validate({"name": "Test"})
# Empty nodes array is invalid
invalid_workflow = valid_workflow.copy()
invalid_workflow["nodes"] = []
assert not N8NWorkflow.validate(invalid_workflow)
# Invalid node
invalid_workflow = valid_workflow.copy()
invalid_workflow["nodes"] = [{"id": "bad"}]
assert not N8NWorkflow.validate(invalid_workflow)

View File

@@ -11,6 +11,7 @@ def test_build_workflow_graph_structure():
node_ids = {node["id"] for node in graph["nodes"]}
for edge in graph["edges"]:
assert edge["from"] in node_ids
assert edge["to"] in node_ids
assert isinstance(edge["var"], str) and edge["var"], "edges should reference a variable"
assert edge["from"] in node_ids, f"Edge source {edge['from']} not in node IDs"
assert edge["to"] in node_ids, f"Edge target {edge['to']} not in node IDs"
# N8N format uses 'type' instead of 'var'
assert "type" in edge or "var" in edge, "edges should have connection type or variable"