Skip to content

Commit 07dc91c

Browse files
Merge pull request #1399 from MervinPraison/claude/issue-1397-20260416-1206
feat: implement n8n Visual Workflow Editor integration
2 parents 2d05e6f + 94094a8 commit 07dc91c

9 files changed

Lines changed: 1841 additions & 187 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: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
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 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 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+
client = None
251+
try:
252+
from praisonai.n8n import N8nClient
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+
except typer.Exit:
271+
raise
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+
finally:
279+
if client is not None:
280+
client.close()
281+
282+
@app.command(name="list")
283+
def list_workflows(
284+
n8n_url: str = typer.Option("http://localhost:5678", "--n8n-url", help="n8n instance URL"),
285+
api_key: Optional[str] = typer.Option(None, "--api-key", help="n8n API key (or set N8N_API_KEY env var)")
286+
):
287+
"""List workflows in n8n instance.
288+
289+
Example:
290+
praisonai n8n list
291+
"""
292+
client = None
293+
try:
294+
from praisonai.n8n import N8nClient
295+
296+
client = N8nClient(base_url=n8n_url, api_key=api_key)
297+
298+
workflows = client.list_workflows()
299+
300+
if not workflows:
301+
typer.echo("No workflows found in n8n")
302+
else:
303+
typer.echo(f"Found {len(workflows)} workflows:")
304+
typer.echo()
305+
306+
for workflow in workflows:
307+
workflow_id = workflow.get('id', 'Unknown')
308+
name = workflow.get('name', 'Untitled')
309+
active = workflow.get('active', False)
310+
status = "✅ Active" if active else "⏸️ Inactive"
311+
312+
typer.echo(f" {workflow_id}: {name} ({status})")
313+
314+
except ImportError:
315+
typer.echo("Error: n8n dependencies not installed. Run: pip install 'praisonai[n8n]'", err=True)
316+
raise typer.Exit(1)
317+
except ConnectionError as e:
318+
typer.echo(f"Connection Error: {e}", err=True)
319+
typer.echo("💡 Make sure n8n is running and accessible")
320+
raise typer.Exit(1)
321+
except Exception as e:
322+
typer.echo(f"Error: {e}", err=True)
323+
raise typer.Exit(1)
324+
finally:
325+
if client is not None:
326+
client.close()

0 commit comments

Comments
 (0)