Files
tkb_timeshift/claude_n8n/tools/manual_trigger_manager.py
Docker Config Backup 8793ac4f59 Add Claude N8N toolkit with Docker mock API server
- 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>
2025-06-17 21:23:46 +02:00

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