""" 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()