Skip to content

Commit 05dd2a3

Browse files
feat: implement n8n Visual Workflow Editor integration (fixes #1397)
- Add complete n8n package for bidirectional YAML ↔ n8n JSON conversion - Implement YAMLToN8nConverter for PraisonAI workflows → n8n format - Implement N8nToYAMLConverter for reverse conversion - Add N8nClient for n8n API integration (CRUD operations) - Add preview system with browser integration - Support agent-to-node mapping with tools/control flow - Add comprehensive CLI commands (export, import, preview, push, pull, test, list) - Add n8n optional dependency and comprehensive test coverage - Register n8n commands with main CLI application This enables visual workflow editing in n8n while keeping YAML as source of truth. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: MervinPraison <MervinPraison@users.noreply.github.com>
1 parent 7e5ce3a commit 05dd2a3

9 files changed

Lines changed: 1802 additions & 188 deletions

File tree

src/praisonai/praisonai/cli/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ def register_commands():
241241
from .commands.memory import app as memory_app
242242
from .commands.workflow import app as workflow_app
243243
from .commands.tools import app as tools_app
244+
from .commands.n8n import app as n8n_app
244245
from .commands.knowledge import app as knowledge_app
245246
from .commands.rag import app as rag_app
246247
from .commands import retrieval as retrieval_module
@@ -318,6 +319,7 @@ def register_commands():
318319
app.add_typer(memory_app, name="memory", help="Memory management")
319320
app.add_typer(workflow_app, name="workflow", help="Workflow management")
320321
app.add_typer(tools_app, name="tools", help="Tool management")
322+
app.add_typer(n8n_app, name="n8n", help="n8n visual workflow editor integration")
321323
app.add_typer(knowledge_app, name="knowledge", help="Knowledge base management (legacy)")
322324
app.add_typer(rag_app, name="rag", help="RAG commands (legacy - use index/query instead)")
323325

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
"""
2+
n8n CLI Commands
3+
4+
CLI commands for n8n workflow integration.
5+
"""
6+
7+
import typer
8+
import logging
9+
from pathlib import Path
10+
from typing import Optional
11+
12+
logger = logging.getLogger(__name__)
13+
14+
app = typer.Typer(
15+
name="n8n",
16+
help="n8n visual workflow editor integration commands",
17+
no_args_is_help=True,
18+
rich_markup_mode="rich"
19+
)
20+
21+
@app.command()
22+
def export(
23+
yaml_path: Path = typer.Argument(..., help="Path to YAML workflow file"),
24+
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output JSON file path"),
25+
format: str = typer.Option("n8n", "--format", help="Export format (currently only n8n supported)")
26+
):
27+
"""Export PraisonAI YAML workflow to n8n JSON format.
28+
29+
Example:
30+
praisonai n8n export my-workflow.yaml --output workflow.json
31+
"""
32+
if format != 'n8n':
33+
typer.echo(f"Error: Unsupported format '{format}'. Only 'n8n' is supported.", err=True)
34+
raise typer.Exit(1)
35+
36+
try:
37+
from praisonai.n8n import YAMLToN8nConverter
38+
import yaml as yaml_lib
39+
import json
40+
41+
# Load YAML workflow
42+
with open(yaml_path, 'r') as f:
43+
yaml_workflow = yaml_lib.safe_load(f)
44+
45+
# Convert to n8n format
46+
converter = YAMLToN8nConverter()
47+
n8n_json = converter.convert(yaml_workflow)
48+
49+
# Determine output path
50+
if output is None:
51+
output = yaml_path.with_suffix('.json')
52+
53+
# Write JSON file
54+
with open(output, 'w') as f:
55+
json.dump(n8n_json, f, indent=2)
56+
57+
typer.echo(f"✅ Exported workflow to: {output}")
58+
typer.echo(f"💡 Import this file into n8n or use 'praisonai n8n preview {yaml_path}' to open directly")
59+
60+
except ImportError:
61+
typer.echo("Error: n8n dependencies not installed. Run: pip install 'praisonai[n8n]'", err=True)
62+
raise typer.Exit(1)
63+
except Exception as e:
64+
typer.echo(f"Error: {e}", err=True)
65+
raise typer.Exit(1)
66+
67+
@app.command(name="import")
68+
def import_workflow(
69+
json_path: Path = typer.Argument(..., help="Path to n8n JSON workflow file"),
70+
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output YAML file path"),
71+
format: str = typer.Option("n8n", "--format", help="Import format (currently only n8n supported)")
72+
):
73+
"""Import n8n JSON workflow to PraisonAI YAML format.
74+
75+
Example:
76+
praisonai n8n import workflow.json --output my-workflow.yaml
77+
"""
78+
if format != 'n8n':
79+
typer.echo(f"Error: Unsupported format '{format}'. Only 'n8n' is supported.", err=True)
80+
raise typer.Exit(1)
81+
82+
try:
83+
from praisonai.n8n import N8nToYAMLConverter
84+
import yaml as yaml_lib
85+
import json
86+
87+
# Load n8n JSON workflow
88+
with open(json_path, 'r') as f:
89+
n8n_workflow = json.load(f)
90+
91+
# Convert to YAML format
92+
converter = N8nToYAMLConverter()
93+
yaml_workflow = converter.convert(n8n_workflow)
94+
95+
# Determine output path
96+
if output is None:
97+
output = json_path.with_suffix('.yaml')
98+
99+
# Write YAML file
100+
with open(output, 'w') as f:
101+
yaml_lib.dump(yaml_workflow, f, default_flow_style=False, sort_keys=False)
102+
103+
typer.echo(f"✅ Imported workflow to: {output}")
104+
typer.echo(f"💡 Run 'praisonai workflow run {output}' to execute the workflow")
105+
106+
except ImportError:
107+
typer.echo("Error: n8n dependencies not installed. Run: pip install 'praisonai[n8n]'", err=True)
108+
raise typer.Exit(1)
109+
except Exception as e:
110+
typer.echo(f"Error: {e}", err=True)
111+
raise typer.Exit(1)
112+
113+
@app.command()
114+
def preview(
115+
yaml_path: Path = typer.Argument(..., help="Path to YAML workflow file"),
116+
n8n_url: str = typer.Option("http://localhost:5678", "--n8n-url", help="n8n instance URL"),
117+
api_key: Optional[str] = typer.Option(None, "--api-key", help="n8n API key (or set N8N_API_KEY env var)"),
118+
no_open: bool = typer.Option(False, "--no-open", help="Do not automatically open browser")
119+
):
120+
"""Preview PraisonAI workflow in n8n visual editor.
121+
122+
This command converts your YAML workflow to n8n format and opens it
123+
in the n8n visual editor for preview and editing.
124+
125+
Example:
126+
praisonai n8n preview my-workflow.yaml
127+
praisonai n8n preview my-workflow.yaml --n8n-url http://n8n.example.com
128+
"""
129+
try:
130+
from praisonai.n8n import preview_workflow
131+
132+
# Preview workflow in n8n
133+
editor_url = preview_workflow(
134+
yaml_path=str(yaml_path),
135+
n8n_url=n8n_url,
136+
api_key=api_key,
137+
auto_open=not no_open
138+
)
139+
140+
typer.echo(f"✅ Workflow created in n8n")
141+
typer.echo(f"🌐 Editor URL: {editor_url}")
142+
143+
if no_open:
144+
typer.echo(f"💡 Open the URL above to view/edit your workflow visually")
145+
else:
146+
typer.echo(f"💡 Browser should open automatically. If not, visit the URL above")
147+
148+
except ImportError:
149+
typer.echo("Error: n8n dependencies not installed. Run: pip install 'praisonai[n8n]'", err=True)
150+
raise typer.Exit(1)
151+
except FileNotFoundError as e:
152+
typer.echo(f"Error: {e}", err=True)
153+
raise typer.Exit(1)
154+
except ConnectionError as e:
155+
typer.echo(f"Connection Error: {e}", err=True)
156+
typer.echo("💡 Make sure n8n is running. Start with: npx n8n start")
157+
raise typer.Exit(1)
158+
except Exception as e:
159+
typer.echo(f"Error: {e}", err=True)
160+
raise typer.Exit(1)
161+
162+
@app.command()
163+
def pull(
164+
workflow_id: str = typer.Argument(..., help="n8n workflow ID"),
165+
output_path: Path = typer.Argument(..., help="Path where to save YAML file"),
166+
n8n_url: str = typer.Option("http://localhost:5678", "--n8n-url", help="n8n instance URL"),
167+
api_key: Optional[str] = typer.Option(None, "--api-key", help="n8n API key (or set N8N_API_KEY env var)")
168+
):
169+
"""Pull workflow from n8n and convert to YAML.
170+
171+
Example:
172+
praisonai n8n pull abc123 my-workflow.yaml
173+
"""
174+
try:
175+
from praisonai.n8n import export_from_n8n
176+
177+
# Export from n8n to YAML
178+
export_from_n8n(
179+
workflow_id=workflow_id,
180+
output_path=str(output_path),
181+
n8n_url=n8n_url,
182+
api_key=api_key
183+
)
184+
185+
typer.echo(f"✅ Pulled workflow {workflow_id} to: {output_path}")
186+
typer.echo(f"💡 Run 'praisonai workflow run {output_path}' to execute the workflow")
187+
188+
except ImportError:
189+
typer.echo("Error: n8n dependencies not installed. Run: pip install 'praisonai[n8n]'", err=True)
190+
raise typer.Exit(1)
191+
except ConnectionError as e:
192+
typer.echo(f"Connection Error: {e}", err=True)
193+
typer.echo("💡 Make sure n8n is running and accessible")
194+
raise typer.Exit(1)
195+
except Exception as e:
196+
typer.echo(f"Error: {e}", err=True)
197+
raise typer.Exit(1)
198+
199+
@app.command()
200+
def push(
201+
yaml_path: Path = typer.Argument(..., help="Path to YAML workflow file"),
202+
workflow_id: str = typer.Argument(..., help="Existing n8n workflow ID to update"),
203+
n8n_url: str = typer.Option("http://localhost:5678", "--n8n-url", help="n8n instance URL"),
204+
api_key: Optional[str] = typer.Option(None, "--api-key", help="n8n API key (or set N8N_API_KEY env var)")
205+
):
206+
"""Push YAML workflow updates to existing n8n workflow.
207+
208+
Example:
209+
praisonai n8n push my-workflow.yaml abc123
210+
"""
211+
try:
212+
from praisonai.n8n import sync_workflow
213+
214+
# Sync workflow with n8n
215+
editor_url = sync_workflow(
216+
yaml_path=str(yaml_path),
217+
workflow_id=workflow_id,
218+
n8n_url=n8n_url,
219+
api_key=api_key
220+
)
221+
222+
typer.echo(f"✅ Synced workflow {workflow_id}")
223+
typer.echo(f"🌐 Editor URL: {editor_url}")
224+
225+
except ImportError:
226+
typer.echo("Error: n8n dependencies not installed. Run: pip install 'praisonai[n8n]'", err=True)
227+
raise typer.Exit(1)
228+
except FileNotFoundError as e:
229+
typer.echo(f"Error: {e}", err=True)
230+
raise typer.Exit(1)
231+
except ConnectionError as e:
232+
typer.echo(f"Connection Error: {e}", err=True)
233+
typer.echo("💡 Make sure n8n is running and accessible")
234+
raise typer.Exit(1)
235+
except Exception as e:
236+
typer.echo(f"Error: {e}", err=True)
237+
raise typer.Exit(1)
238+
239+
@app.command(name="test")
240+
def test_connection(
241+
n8n_url: str = typer.Option("http://localhost:5678", "--n8n-url", help="n8n instance URL"),
242+
api_key: Optional[str] = typer.Option(None, "--api-key", help="n8n API key (or set N8N_API_KEY env var)")
243+
):
244+
"""Test connection to n8n instance.
245+
246+
Example:
247+
praisonai n8n test
248+
praisonai n8n test --n8n-url http://n8n.example.com
249+
"""
250+
try:
251+
from praisonai.n8n import N8nClient
252+
253+
client = N8nClient(base_url=n8n_url, api_key=api_key)
254+
255+
if client.test_connection():
256+
typer.echo(f"✅ Connected to n8n at {n8n_url}")
257+
258+
# Try to list workflows to test API access
259+
try:
260+
workflows = client.list_workflows()
261+
typer.echo(f"📊 Found {len(workflows)} workflows")
262+
except Exception as e:
263+
typer.echo(f"⚠️ Connection successful but API access failed: {e}")
264+
typer.echo("💡 Check your API key or n8n permissions")
265+
else:
266+
typer.echo(f"❌ Cannot connect to n8n at {n8n_url}")
267+
typer.echo("💡 Make sure n8n is running. Start with: npx n8n start")
268+
raise typer.Exit(1)
269+
270+
client.close()
271+
272+
except ImportError:
273+
typer.echo("Error: n8n dependencies not installed. Run: pip install 'praisonai[n8n]'", err=True)
274+
raise typer.Exit(1)
275+
except Exception as e:
276+
typer.echo(f"Error: {e}", err=True)
277+
raise typer.Exit(1)
278+
279+
@app.command(name="list")
280+
def list_workflows(
281+
n8n_url: str = typer.Option("http://localhost:5678", "--n8n-url", help="n8n instance URL"),
282+
api_key: Optional[str] = typer.Option(None, "--api-key", help="n8n API key (or set N8N_API_KEY env var)")
283+
):
284+
"""List workflows in n8n instance.
285+
286+
Example:
287+
praisonai n8n list
288+
"""
289+
try:
290+
from praisonai.n8n import N8nClient
291+
292+
client = N8nClient(base_url=n8n_url, api_key=api_key)
293+
294+
workflows = client.list_workflows()
295+
296+
if not workflows:
297+
typer.echo("No workflows found in n8n")
298+
else:
299+
typer.echo(f"Found {len(workflows)} workflows:")
300+
typer.echo()
301+
302+
for workflow in workflows:
303+
workflow_id = workflow.get('id', 'Unknown')
304+
name = workflow.get('name', 'Untitled')
305+
active = workflow.get('active', False)
306+
status = "✅ Active" if active else "⏸️ Inactive"
307+
308+
typer.echo(f" {workflow_id}: {name} ({status})")
309+
310+
client.close()
311+
312+
except ImportError:
313+
typer.echo("Error: n8n dependencies not installed. Run: pip install 'praisonai[n8n]'", err=True)
314+
raise typer.Exit(1)
315+
except ConnectionError as e:
316+
typer.echo(f"Connection Error: {e}", err=True)
317+
typer.echo("💡 Make sure n8n is running and accessible")
318+
raise typer.Exit(1)
319+
except Exception as e:
320+
typer.echo(f"Error: {e}", err=True)
321+
raise typer.Exit(1)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
n8n Integration for PraisonAI Workflows
3+
4+
This module provides bidirectional conversion between PraisonAI YAML workflows
5+
and n8n JSON format for visual workflow editing.
6+
7+
Features:
8+
- Convert PraisonAI YAML workflows to n8n JSON format
9+
- Reverse conversion from n8n JSON back to YAML
10+
- CLI commands for export, import, and preview
11+
- n8n API integration for workflow management
12+
13+
Example:
14+
from praisonai.n8n import YAMLToN8nConverter, preview_workflow
15+
16+
converter = YAMLToN8nConverter()
17+
n8n_json = converter.convert(yaml_workflow)
18+
19+
# Preview in n8n UI
20+
preview_workflow("my-workflow.yaml")
21+
"""
22+
23+
from typing import TYPE_CHECKING
24+
25+
if TYPE_CHECKING:
26+
from .converter import YAMLToN8nConverter
27+
from .reverse_converter import N8nToYAMLConverter
28+
from .preview import preview_workflow
29+
from .client import N8nClient
30+
31+
# Lazy imports for optional dependencies
32+
def __getattr__(name: str):
33+
if name == "YAMLToN8nConverter":
34+
from .converter import YAMLToN8nConverter
35+
return YAMLToN8nConverter
36+
elif name == "N8nToYAMLConverter":
37+
from .reverse_converter import N8nToYAMLConverter
38+
return N8nToYAMLConverter
39+
elif name == "preview_workflow":
40+
from .preview import preview_workflow
41+
return preview_workflow
42+
elif name == "N8nClient":
43+
from .client import N8nClient
44+
return N8nClient
45+
else:
46+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
47+
48+
__all__ = [
49+
"YAMLToN8nConverter",
50+
"N8nToYAMLConverter",
51+
"preview_workflow",
52+
"N8nClient",
53+
]

0 commit comments

Comments
 (0)