- Added comprehensive N8N development tools collection - Added Docker-containerized mock API server for testing - Added complete documentation and setup guides - Added mock API server with health checks and data endpoints - Tools include workflow analyzers, debuggers, and controllers 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
456 lines
17 KiB
Python
456 lines
17 KiB
Python
"""
|
|
Manual Trigger Manager for N8N Workflows
|
|
|
|
This module provides functionality to manage manual triggers for N8N workflows,
|
|
including webhook-based triggers and manual execution capabilities.
|
|
"""
|
|
|
|
import json
|
|
import uuid
|
|
import logging
|
|
from typing import Dict, List, Any, Optional
|
|
from .n8n_client import N8NClient
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class ManualTriggerManager:
|
|
"""Manager for creating and handling manual triggers in N8N workflows."""
|
|
|
|
def __init__(self, client: Optional[N8NClient] = None):
|
|
"""
|
|
Initialize the manual trigger manager.
|
|
|
|
Args:
|
|
client: N8N client instance. If None, creates a new one.
|
|
"""
|
|
self.client = client or N8NClient()
|
|
|
|
def add_manual_trigger_to_workflow(self, workflow_id: str,
|
|
trigger_type: str = "manual") -> Dict[str, Any]:
|
|
"""
|
|
Add a manual trigger to an existing workflow.
|
|
|
|
Args:
|
|
workflow_id: ID of the workflow to modify
|
|
trigger_type: Type of trigger ('manual', 'webhook', 'http')
|
|
|
|
Returns:
|
|
Result of the operation including trigger details
|
|
"""
|
|
try:
|
|
workflow = self.client.get_workflow(workflow_id)
|
|
workflow_name = workflow.get('name', 'Unknown')
|
|
|
|
# Check if workflow already has the specified trigger type
|
|
nodes = workflow.get('nodes', [])
|
|
existing_trigger = self._find_trigger_node(nodes, trigger_type)
|
|
|
|
if existing_trigger:
|
|
return {
|
|
'success': True,
|
|
'message': f"Workflow {workflow_name} already has a {trigger_type} trigger",
|
|
'trigger_node': existing_trigger,
|
|
'added_new_trigger': False
|
|
}
|
|
|
|
# Create the appropriate trigger node
|
|
trigger_node = self._create_trigger_node(trigger_type, workflow_id)
|
|
|
|
# Add trigger node to workflow
|
|
updated_nodes = [trigger_node] + nodes
|
|
|
|
# Update connections to connect trigger to first existing node
|
|
connections = workflow.get('connections', {})
|
|
if nodes and not self._has_trigger_connections(connections):
|
|
first_node_name = nodes[0].get('name')
|
|
if first_node_name:
|
|
trigger_name = trigger_node['name']
|
|
if trigger_name not in connections:
|
|
connections[trigger_name] = {}
|
|
connections[trigger_name]['main'] = [[{
|
|
'node': first_node_name,
|
|
'type': 'main',
|
|
'index': 0
|
|
}]]
|
|
|
|
# Update workflow
|
|
updated_workflow = {
|
|
**workflow,
|
|
'nodes': updated_nodes,
|
|
'connections': connections
|
|
}
|
|
|
|
result = self.client.update_workflow(workflow_id, updated_workflow)
|
|
|
|
logger.info(f"Added {trigger_type} trigger to workflow: {workflow_name} ({workflow_id})")
|
|
|
|
return {
|
|
'success': True,
|
|
'message': f"Successfully added {trigger_type} trigger to {workflow_name}",
|
|
'trigger_node': trigger_node,
|
|
'workflow': result,
|
|
'added_new_trigger': True
|
|
}
|
|
|
|
except Exception as e:
|
|
error_msg = f"Failed to add {trigger_type} trigger to workflow {workflow_id}: {e}"
|
|
logger.error(error_msg)
|
|
return {
|
|
'success': False,
|
|
'error': error_msg,
|
|
'workflow_id': workflow_id
|
|
}
|
|
|
|
def create_webhook_trigger_workflow(self, workflow_name: str,
|
|
webhook_path: Optional[str] = None) -> Dict[str, Any]:
|
|
"""
|
|
Create a new workflow with a webhook trigger.
|
|
|
|
Args:
|
|
workflow_name: Name for the new workflow
|
|
webhook_path: Custom webhook path (if None, generates random)
|
|
|
|
Returns:
|
|
Created workflow with webhook details
|
|
"""
|
|
try:
|
|
if not webhook_path:
|
|
webhook_path = f"test-webhook-{str(uuid.uuid4())[:8]}"
|
|
|
|
# Create webhook trigger node
|
|
webhook_node = {
|
|
"id": str(uuid.uuid4()),
|
|
"name": "Webhook Trigger",
|
|
"type": "n8n-nodes-base.webhook",
|
|
"typeVersion": 1,
|
|
"position": [100, 100],
|
|
"webhookId": str(uuid.uuid4()),
|
|
"parameters": {
|
|
"httpMethod": "POST",
|
|
"path": webhook_path,
|
|
"responseMode": "responseNode",
|
|
"options": {
|
|
"noResponseBody": False
|
|
}
|
|
}
|
|
}
|
|
|
|
# Create a simple response node
|
|
response_node = {
|
|
"id": str(uuid.uuid4()),
|
|
"name": "Response",
|
|
"type": "n8n-nodes-base.respondToWebhook",
|
|
"typeVersion": 1,
|
|
"position": [300, 100],
|
|
"parameters": {
|
|
"options": {}
|
|
}
|
|
}
|
|
|
|
# Create workflow structure
|
|
workflow_data = {
|
|
"name": workflow_name,
|
|
"active": False, # Start inactive for testing
|
|
"nodes": [webhook_node, response_node],
|
|
"connections": {
|
|
"Webhook Trigger": {
|
|
"main": [[{
|
|
"node": "Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}]]
|
|
}
|
|
},
|
|
"settings": {
|
|
"timezone": "UTC"
|
|
}
|
|
}
|
|
|
|
# Create the workflow
|
|
created_workflow = self.client.create_workflow(workflow_data)
|
|
|
|
# Get N8N base URL for webhook URL construction
|
|
webhook_url = f"{self.client.base_url.replace('/api/v1', '')}/webhook-test/{webhook_path}"
|
|
|
|
logger.info(f"Created webhook trigger workflow: {workflow_name}")
|
|
|
|
return {
|
|
'success': True,
|
|
'workflow': created_workflow,
|
|
'webhook_url': webhook_url,
|
|
'webhook_path': webhook_path,
|
|
'test_command': f"curl -X POST {webhook_url} -H 'Content-Type: application/json' -d '{{}}'",
|
|
'message': f"Created workflow {workflow_name} with webhook trigger"
|
|
}
|
|
|
|
except Exception as e:
|
|
error_msg = f"Failed to create webhook trigger workflow: {e}"
|
|
logger.error(error_msg)
|
|
return {
|
|
'success': False,
|
|
'error': error_msg
|
|
}
|
|
|
|
def execute_workflow_manually(self, workflow_id: str,
|
|
input_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
"""
|
|
Execute a workflow manually with optional input data.
|
|
|
|
Args:
|
|
workflow_id: ID of the workflow to execute
|
|
input_data: Optional input data for the execution
|
|
|
|
Returns:
|
|
Execution result
|
|
"""
|
|
try:
|
|
# Execute workflow
|
|
if input_data:
|
|
execution = self.client.execute_workflow(workflow_id, input_data)
|
|
else:
|
|
execution = self.client.execute_workflow(workflow_id)
|
|
|
|
logger.info(f"Manually executed workflow: {workflow_id}")
|
|
|
|
return {
|
|
'success': True,
|
|
'execution': execution,
|
|
'execution_id': execution.get('id'),
|
|
'message': f"Successfully executed workflow {workflow_id}"
|
|
}
|
|
|
|
except Exception as e:
|
|
error_msg = f"Failed to execute workflow {workflow_id} manually: {e}"
|
|
logger.error(error_msg)
|
|
return {
|
|
'success': False,
|
|
'error': error_msg,
|
|
'workflow_id': workflow_id
|
|
}
|
|
|
|
def trigger_webhook_workflow(self, webhook_url: str,
|
|
data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
"""
|
|
Trigger a webhook-enabled workflow.
|
|
|
|
Args:
|
|
webhook_url: URL of the webhook
|
|
data: Data to send to webhook
|
|
|
|
Returns:
|
|
Webhook response
|
|
"""
|
|
import requests
|
|
|
|
try:
|
|
if data is None:
|
|
data = {"test": True, "timestamp": "2024-01-01T00:00:00Z"}
|
|
|
|
response = requests.post(
|
|
webhook_url,
|
|
json=data,
|
|
headers={'Content-Type': 'application/json'},
|
|
timeout=30
|
|
)
|
|
|
|
logger.info(f"Triggered webhook: {webhook_url}")
|
|
|
|
return {
|
|
'success': True,
|
|
'status_code': response.status_code,
|
|
'response_data': response.json() if response.headers.get('content-type', '').startswith('application/json') else response.text,
|
|
'webhook_url': webhook_url,
|
|
'sent_data': data
|
|
}
|
|
|
|
except Exception as e:
|
|
error_msg = f"Failed to trigger webhook {webhook_url}: {e}"
|
|
logger.error(error_msg)
|
|
return {
|
|
'success': False,
|
|
'error': error_msg,
|
|
'webhook_url': webhook_url
|
|
}
|
|
|
|
def setup_test_workflow(self, workflow_id: str,
|
|
test_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
"""
|
|
Set up a workflow for testing by ensuring it has manual trigger and is inactive.
|
|
|
|
Args:
|
|
workflow_id: ID of the workflow to set up
|
|
test_data: Optional test data to associate with the workflow
|
|
|
|
Returns:
|
|
Setup result with testing instructions
|
|
"""
|
|
try:
|
|
workflow = self.client.get_workflow(workflow_id)
|
|
workflow_name = workflow.get('name', 'Unknown')
|
|
|
|
# Ensure workflow is inactive
|
|
if workflow.get('active', False):
|
|
updated_workflow = {**workflow, 'active': False}
|
|
workflow = self.client.update_workflow(workflow_id, updated_workflow)
|
|
logger.info(f"Set workflow {workflow_name} to inactive for testing")
|
|
|
|
# Add manual trigger if not present
|
|
trigger_result = self.add_manual_trigger_to_workflow(workflow_id, 'manual')
|
|
|
|
# Save test data if provided
|
|
test_info = {}
|
|
if test_data:
|
|
from .mock_data_generator import MockDataGenerator
|
|
data_gen = MockDataGenerator()
|
|
test_file = data_gen.save_mock_data([test_data], f"test_data_{workflow_id}")
|
|
test_info['test_data_file'] = test_file
|
|
|
|
return {
|
|
'success': True,
|
|
'workflow_name': workflow_name,
|
|
'workflow_id': workflow_id,
|
|
'is_inactive': True,
|
|
'has_manual_trigger': True,
|
|
'trigger_setup': trigger_result,
|
|
'test_info': test_info,
|
|
'testing_instructions': {
|
|
'manual_execution': f"Use execute_workflow_manually('{workflow_id}') to test",
|
|
'monitor_logs': "Use DockerLogMonitor to watch execution logs",
|
|
'check_results': f"Use client.get_executions(workflow_id='{workflow_id}') to see results"
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
error_msg = f"Failed to set up workflow for testing {workflow_id}: {e}"
|
|
logger.error(error_msg)
|
|
return {
|
|
'success': False,
|
|
'error': error_msg,
|
|
'workflow_id': workflow_id
|
|
}
|
|
|
|
def _find_trigger_node(self, nodes: List[Dict[str, Any]],
|
|
trigger_type: str) -> Optional[Dict[str, Any]]:
|
|
"""Find a trigger node of specific type in the nodes list."""
|
|
trigger_types = {
|
|
'manual': 'n8n-nodes-base.manualTrigger',
|
|
'webhook': 'n8n-nodes-base.webhook',
|
|
'http': 'n8n-nodes-base.httpRequest'
|
|
}
|
|
|
|
target_type = trigger_types.get(trigger_type)
|
|
if not target_type:
|
|
return None
|
|
|
|
for node in nodes:
|
|
if node.get('type') == target_type:
|
|
return node
|
|
|
|
return None
|
|
|
|
def _create_trigger_node(self, trigger_type: str, workflow_id: str) -> Dict[str, Any]:
|
|
"""Create a trigger node of the specified type."""
|
|
node_id = str(uuid.uuid4())
|
|
|
|
if trigger_type == 'manual':
|
|
return {
|
|
"id": node_id,
|
|
"name": "Manual Trigger",
|
|
"type": "n8n-nodes-base.manualTrigger",
|
|
"typeVersion": 1,
|
|
"position": [50, 100],
|
|
"parameters": {}
|
|
}
|
|
|
|
elif trigger_type == 'webhook':
|
|
webhook_path = f"test-{workflow_id[:8]}-{str(uuid.uuid4())[:8]}"
|
|
return {
|
|
"id": node_id,
|
|
"name": "Webhook Trigger",
|
|
"type": "n8n-nodes-base.webhook",
|
|
"typeVersion": 1,
|
|
"position": [50, 100],
|
|
"webhookId": str(uuid.uuid4()),
|
|
"parameters": {
|
|
"httpMethod": "POST",
|
|
"path": webhook_path,
|
|
"responseMode": "responseNode",
|
|
"options": {
|
|
"noResponseBody": False
|
|
}
|
|
}
|
|
}
|
|
|
|
elif trigger_type == 'http':
|
|
return {
|
|
"id": node_id,
|
|
"name": "HTTP Request",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4,
|
|
"position": [50, 100],
|
|
"parameters": {
|
|
"method": "GET",
|
|
"url": "https://httpbin.org/json",
|
|
"options": {}
|
|
}
|
|
}
|
|
|
|
else:
|
|
raise ValueError(f"Unsupported trigger type: {trigger_type}")
|
|
|
|
def _has_trigger_connections(self, connections: Dict[str, Any]) -> bool:
|
|
"""Check if connections already include trigger nodes."""
|
|
trigger_names = ['Manual Trigger', 'Webhook Trigger', 'HTTP Request']
|
|
return any(name in connections for name in trigger_names)
|
|
|
|
def get_workflow_triggers(self, workflow_id: str) -> List[Dict[str, Any]]:
|
|
"""
|
|
Get all trigger nodes in a workflow.
|
|
|
|
Args:
|
|
workflow_id: ID of the workflow
|
|
|
|
Returns:
|
|
List of trigger nodes
|
|
"""
|
|
try:
|
|
workflow = self.client.get_workflow(workflow_id)
|
|
nodes = workflow.get('nodes', [])
|
|
|
|
trigger_types = [
|
|
'n8n-nodes-base.manualTrigger',
|
|
'n8n-nodes-base.webhook',
|
|
'n8n-nodes-base.httpRequest',
|
|
'n8n-nodes-base.cron',
|
|
'n8n-nodes-base.interval'
|
|
]
|
|
|
|
triggers = []
|
|
for node in nodes:
|
|
if node.get('type') in trigger_types:
|
|
trigger_info = {
|
|
'id': node.get('id'),
|
|
'name': node.get('name'),
|
|
'type': node.get('type'),
|
|
'parameters': node.get('parameters', {}),
|
|
'position': node.get('position', [0, 0])
|
|
}
|
|
|
|
# Add webhook-specific info
|
|
if node.get('type') == 'n8n-nodes-base.webhook':
|
|
webhook_path = node.get('parameters', {}).get('path', '')
|
|
webhook_url = f"{self.client.base_url.replace('/api/v1', '')}/webhook-test/{webhook_path}"
|
|
trigger_info['webhook_url'] = webhook_url
|
|
trigger_info['webhook_path'] = webhook_path
|
|
|
|
triggers.append(trigger_info)
|
|
|
|
return triggers
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to get workflow triggers for {workflow_id}: {e}")
|
|
return []
|
|
|
|
def create_manual_trigger_manager():
|
|
"""Create a manual trigger manager instance."""
|
|
return ManualTriggerManager() |