Files
swarm-zap/skills/n8n-webhook/assets/openclaw-action.workflow.json

219 lines
56 KiB
JSON

{
"name": "openclaw-action",
"nodes": [
{
"id": "webhook-openclaw-action",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-700,
40
],
"parameters": {
"httpMethod": "POST",
"path": "openclaw-action",
"authentication": "none",
"responseMode": "responseNode",
"options": {}
}
},
{
"id": "route-action",
"name": "route-action",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-420,
40
],
"parameters": {
"mode": "runOnceForEachItem",
"language": "javaScript",
"jsCode": "return (async () => {\n const body = ($json.body && typeof $json.body === 'object') ? $json.body : $json;\n const action = typeof body.action === 'string' ? body.action : '';\n const args = (body.args && typeof body.args === 'object' && body.args !== null) ? body.args : {};\n const requestId = typeof body.request_id === 'string' ? body.request_id : '';\n const now = new Date().toISOString();\n const workflowStaticData = $getWorkflowStaticData('global');\n const maxLogEntries = 200;\n const maxQueueEntries = 200;\n const maxHistoryEntries = 200;\n const maxEventEntries = 200;\n const maxDedupEntries = 500;\n\n const clamp = (value, min, max, fallback) => {\n const num = Number(value);\n if (!Number.isFinite(num)) {\n return fallback;\n }\n return Math.max(min, Math.min(max, Math.trunc(num)));\n };\n\n const asString = (value, fallback = '') => (typeof value === 'string' ? value : fallback);\n\n const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);\n\n const trimText = (value) => asString(value).trim();\n\n const previewText = (value, max = 160) => {\n const text = trimText(value).replace(/\\s+/g, ' ');\n if (text.length <= max) {\n return text;\n }\n return `${text.slice(0, Math.max(0, max - 1)).trimEnd()}\u2026`;\n };\n\n const ensureStringArray = (value) => {\n if (Array.isArray(value)) {\n return value.map((entry) => trimText(entry)).filter(Boolean);\n }\n if (typeof value === 'string') {\n const trimmed = trimText(value);\n return trimmed ? [trimmed] : [];\n }\n return [];\n };\n\n const pushRetained = (list, item, maxEntries) => {\n const target = Array.isArray(list) ? list : [];\n target.push(item);\n if (target.length > maxEntries) {\n target.splice(0, target.length - maxEntries);\n }\n return target;\n };\n\n const makeId = (prefix) => {\n const rand = Math.random().toString(36).slice(2, 10);\n return `${prefix}-${Date.now().toString(36)}-${rand}`;\n };\n\n const htmlEntityDecode = (value) => {\n const text = asString(value)\n .replace(/&nbsp;/gi, ' ')\n .replace(/&amp;/gi, '&')\n .replace(/&lt;/gi, '<')\n .replace(/&gt;/gi, '>')\n .replace(/&quot;/gi, '\"')\n .replace(/&#39;/gi, \"'\");\n return text;\n };\n\n const stripHtml = (value) => htmlEntityDecode(\n asString(value)\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\s+/g, ' ')\n ).trim();\n\n const normalizeHeaders = (headers) => {\n if (!headers || typeof headers.entries !== 'function') {\n return {};\n }\n return Object.fromEntries(Array.from(headers.entries()).map(([key, value]) => [String(key).toLowerCase(), String(value)]));\n };\n\n const getActionLog = () => Array.isArray(workflowStaticData.actionLog) ? workflowStaticData.actionLog : [];\n const getApprovalQueue = () => Array.isArray(workflowStaticData.approvalQueue) ? workflowStaticData.approvalQueue : [];\n const getApprovalHistory = () => Array.isArray(workflowStaticData.approvalHistory) ? workflowStaticData.approvalHistory : [];\n const getInboundEvents = () => Array.isArray(workflowStaticData.inboundEvents) ? workflowStaticData.inboundEvents : [];\n const getEventDedup = () => Array.isArray(workflowStaticData.eventDedup) ? workflowStaticData.eventDedup : [];\n\n const persistActionLog = (entry) => {\n workflowStaticData.actionLog = pushRetained(getActionLog(), entry, maxLogEntries);\n };\n\n const logAction = (text, meta = undefined) => {\n persistActionLog({\n ts: now,\n source: 'openclaw-action',\n request_id: requestId,\n text,\n meta: isPlainObject(meta) ? meta : undefined,\n });\n };\n\n const defaultActionForKind = (kind) => {\n const mapping = {\n notification: 'send_notification_draft',\n email_draft: 'send_email_draft',\n email_list_drafts: 'list_email_drafts',\n email_draft_delete: 'delete_email_draft',\n email_draft_send: 'send_gmail_draft',\n calendar_event: 'create_calendar_event',\n calendar_list_events: 'list_upcoming_events',\n calendar_event_update: 'update_calendar_event',\n calendar_event_delete: 'delete_calendar_event',\n };\n return mapping[kind] || kind || 'manual';\n };\n\n const actionFamilyForKind = (kind) => {\n if (kind === 'notification') {\n return 'notification';\n }\n if (String(kind || '').startsWith('email_')) {\n return 'gmail';\n }\n if (String(kind || '').startsWith('calendar_')) {\n return 'calendar';\n }\n return 'manual';\n };\n\n const defaultMutationLevelForKind = (kind) => {\n if (['email_list_drafts', 'calendar_list_events'].includes(kind)) {\n return 'low';\n }\n if (['notification', 'email_draft', 'email_draft_delete', 'email_draft_send', 'calendar_event', 'calendar_event_update', 'calendar_event_delete'].includes(kind)) {\n return 'high';\n }\n return 'none';\n };\n\n const makeApprovalPolicy = (mutationLevel = 'none', family = 'manual') => ({\n family,\n policy: 'approval_queue_resolve',\n default_mode: mutationLevel === 'none' ? 'inline_or_manual' : 'queue_then_manual_resolve',\n required: mutationLevel !== 'none',\n mutation_level: mutationLevel,\n reason: family === 'notification'\n ? 'outbound_notification_requires_explicit_approval'\n : family === 'gmail' && mutationLevel === 'low'\n ? 'read_only_gmail_access_still_requires_operator_ack'\n : family === 'gmail'\n ? 'mutating_gmail_action_requires_explicit_approval'\n : family === 'calendar' && mutationLevel === 'low'\n ? 'read_only_calendar_access_still_requires_operator_ack'\n : family === 'calendar'\n ? 'mutating_calendar_action_requires_explicit_approval'\n : mutationLevel !== 'none'\n ? 'manual_operator_review_required'\n : 'read_only_action',\n });\n\n const defaultApprovalPolicyForKind = (kind) => makeApprovalPolicy(\n defaultMutationLevelForKind(kind),\n actionFamilyForKind(kind)\n );\n\n const pickFirstString = (...values) => {\n for (const value of values) {\n const trimmed = trimText(value);\n if (trimmed) {\n return trimmed;\n }\n }\n return '';\n };\n\n const compactPayloadPreview = (kind, payload = {}) => {\n if (!isPlainObject(payload)) {\n return {};\n }\n if (kind === 'notification') {\n return {\n title: trimText(payload.title || ''),\n message_excerpt: previewText(payload.message || '', 160),\n };\n }\n if (kind === 'email_draft') {\n const to = ensureStringArray(payload.to);\n return {\n to,\n cc_count: ensureStringArray(payload.cc).length,\n bcc_count: ensureStringArray(payload.bcc).length,\n subject: trimText(payload.subject || ''),\n body_excerpt: previewText(payload.body_text || stripHtml(payload.body_html || ''), 200),\n };\n }\n if (kind === 'email_list_drafts') {\n return {\n max: clamp(payload.max, 1, 100, 20),\n page: trimText(payload.page || ''),\n all: payload.all === true,\n fail_empty: payload.fail_empty === true,\n };\n }\n if (kind === 'email_draft_delete' || kind === 'email_draft_send') {\n return {\n draft_id: trimText(payload.draft_id || payload.id || ''),\n };\n }\n if (kind === 'calendar_event') {\n return {\n calendar: trimText(payload.calendar || 'primary') || 'primary',\n title: trimText(payload.title || ''),\n start: trimText(payload.start || ''),\n end: trimText(payload.end || ''),\n location: trimText(payload.location || ''),\n attendee_count: ensureStringArray(payload.attendees).length,\n };\n }\n if (kind === 'calendar_list_events') {\n return {\n calendar: trimText(payload.calendar || 'primary') || 'primary',\n max: clamp(payload.max, 1, 100, 20),\n days: clamp(payload.days, 1, 90, 7),\n from: trimText(payload.from || ''),\n to: trimText(payload.to || ''),\n query: trimText(payload.query || ''),\n all_pages: payload.all_pages === true,\n };\n }\n if (kind === 'calendar_event_update') {\n return {\n calendar: trimText(payload.calendar || 'primary') || 'primary',\n event_id: trimText(payload.event_id || payload.id || ''),\n title: trimText(payload.title || ''),\n start: trimText(payload.start || ''),\n end: trimText(payload.end || ''),\n location: trimText(payload.location || ''),\n attendee_count: ensureStringArray(payload.attendees).length,\n send_updates: trimText(payload.send_updates || 'none') || 'none',\n };\n }\n if (kind === 'calendar_event_delete') {\n return {\n calendar: trimText(payload.calendar || 'primary') || 'primary',\n event_id: trimText(payload.event_id || payload.id || ''),\n send_updates: trimText(payload.send_updates || 'none') || 'none',\n };\n }\n return Object.fromEntries(\n Object.entries(payload)\n .filter(([_, value]) => ['string', 'number', 'boolean'].includes(typeof value))\n .slice(0, 8)\n .map(([key, value]) => [key, typeof value === 'string' ? previewText(value, 120) : value])\n );\n };\n\n const extractExecutionRefs = (execution) => {\n if (!isPlainObject(execution)) {\n return {};\n }\n if (isPlainObject(execution.result_refs)) {\n return Object.fromEntries(\n Object.entries(execution.result_refs)\n .map(([key, value]) => [key, trimText(value)])\n .filter(([_, value]) => Boolean(value))\n );\n }\n const result = isPlainObject(execution.result) ? execution.result : {};\n const refs = {};\n const draft = isPlainObject(result.draft) ? result.draft : {};\n const message = isPlainObject(result.message) ? result.message : {};\n const event = isPlainObject(result.event) ? result.event : {};\n const op = trimText(execution.op || '');\n\n const draftId = pickFirstString(\n result.draft_id,\n draft.id,\n op.startsWith('gmail.drafts.') ? result.id : ''\n );\n if (draftId) {\n refs.draft_id = draftId;\n }\n\n const messageId = pickFirstString(result.message_id, message.id);\n if (messageId) {\n refs.message_id = messageId;\n }\n\n const eventId = pickFirstString(\n result.event_id,\n event.id,\n op.startsWith('calendar.') ? result.id : ''\n );\n if (eventId) {\n refs.event_id = eventId;\n }\n\n const calendar = pickFirstString(result.calendar, result.calendar_id, event.calendar);\n if (calendar) {\n refs.calendar = calendar;\n }\n\n return refs;\n };\n\n const summarizeExecutionState = (item) => {\n const status = trimText(item && item.status ? item.status : '');\n const execution = isPlainObject(item && item.execution ? item.execution : null) ? item.execution : null;\n const executionStatus = trimText(execution && execution.status ? execution.status : '');\n if (status === 'rejected') {\n return 'rejected';\n }\n if (status !== 'approved') {\n return status || 'pending';\n }\n if (!execution) {\n return 'awaiting_host_execution';\n }\n if (executionStatus === 'failed') {\n return 'failed';\n }\n if (executionStatus === 'dry_run') {\n return 'dry_run';\n }\n if (executionStatus) {\n return 'executed';\n }\n return 'approved';\n };\n\n const buildSummaryLine = (item) => {\n const summary = trimText(item && item.summary ? item.summary : defaultActionForKind(item && item.kind ? item.kind : 'manual'));\n const executionState = summarizeExecutionState(item);\n const refs = extractExecutionRefs(item && item.execution ? item.execution : null);\n const refBits = [];\n if (refs.draft_id) {\n refBits.push(`draft ${refs.draft_id}`);\n }\n if (refs.message_id) {\n refBits.push(`message ${refs.message_id}`);\n }\n if (refs.event_id) {\n refBits.push(`event ${refs.event_id}`);\n }\n if (executionState === 'rejected') {\n return `rejected \u2014 ${summary}`;\n }\n if (executionState === 'pending') {\n return `pending \u2014 ${summary}`;\n }\n if (executionState === 'dry_run') {\n return `approved + dry_run \u2014 ${summary}${refBits.length ? ` (${refBits.join(', ')})` : ''}`;\n }\n if (executionState === 'failed') {\n return `approved + failed \u2014 ${summary}`;\n }\n if (executionState === 'executed') {\n return `approved + executed \u2014 ${summary}${refBits.length ? ` (${refBits.join(', ')})` : ''}`;\n }\n if (executionState === 'awaiting_host_execution') {\n return `approved + awaiting_host_execution \u2014 ${summary}`;\n }\n return `${executionState} \u2014 ${summary}`;\n };\n\n const buildOperatorView = (item) => {\n const approval = isPlainObject(item.approval) ? item.approval : defaultApprovalPolicyForKind(item.kind);\n const actionName = trimText(item.action || defaultActionForKind(item.kind));\n const requestedAction = trimText(item.requested_action || (isPlainObject(item.payload) ? item.payload.requested_action || '' : '')) || actionName;\n return {\n family: trimText(approval.family || actionFamilyForKind(item.kind)),\n action: actionName,\n requested_action: requestedAction,\n mutation_level: trimText(approval.mutation_level || defaultMutationLevelForKind(item.kind)),\n queue_state: trimText(item.status || 'pending') || 'pending',\n execution_state: summarizeExecutionState(item),\n execution_status: trimText(isPlainObject(item.execution) ? item.execution.status || '' : ''),\n result_refs: extractExecutionRefs(item.execution),\n summary_line: buildSummaryLine(item),\n };\n };\n\n const compactApprovalItem = (item) => {\n const operator = buildOperatorView(item);\n return {\n id: trimText(item.id || ''),\n family: operator.family,\n action: operator.action,\n requested_action: operator.requested_action,\n kind: trimText(item.kind || ''),\n status: trimText(item.status || ''),\n execution_state: operator.execution_state,\n execution_status: operator.execution_status,\n mutation_level: operator.mutation_level,\n summary: trimText(item.summary || ''),\n summary_line: operator.summary_line,\n result_refs: operator.result_refs,\n created_at: trimText(item.created_at || ''),\n resolved_at: trimText(item.resolved_at || ''),\n request_id: trimText(item.request_id || ''),\n tags: Array.isArray(item.tags) ? item.tags.filter(Boolean) : [],\n };\n };\n\n const enqueueApproval = ({ kind, summary, payload, tags = [], actionName = '', requestedAction = '', approval = null }) => {\n const item = {\n id: makeId('approval'),\n action: trimText(actionName || defaultActionForKind(kind)),\n requested_action: trimText(requestedAction || (isPlainObject(payload) ? payload.requested_action || '' : '')) || trimText(actionName || defaultActionForKind(kind)),\n kind,\n status: 'pending',\n created_at: now,\n updated_at: now,\n request_id: requestId,\n summary,\n payload,\n payload_preview: compactPayloadPreview(kind, payload),\n approval: isPlainObject(approval) ? approval : defaultApprovalPolicyForKind(kind),\n tags: Array.isArray(tags) ? tags.filter(Boolean) : [],\n };\n item.operator = buildOperatorView(item);\n workflowStaticData.approvalQueue = pushRetained(getApprovalQueue(), item, maxQueueEntries);\n return item;\n };\n\n const normalizeFetchResponse = ({ inputUrl, response, rawBody, maxChars }) => {\n const headers = normalizeHeaders(response.headers);\n const contentType = headers['content-type'] || '';\n const isHtml = /text\\/html|application\\/xhtml\\+xml/i.test(contentType);\n const isJson = /application\\/json|application\\/ld\\+json|application\\/problem\\+json/i.test(contentType);\n const titleMatch = isHtml ? rawBody.match(/<title[^>]*>([\\s\\S]*?)<\\/title>/i) : null;\n const title = titleMatch ? previewText(stripHtml(titleMatch[1]), 200) : '';\n let bodyText = '';\n if (isHtml) {\n bodyText = stripHtml(rawBody);\n } else if (isJson) {\n try {\n bodyText = JSON.stringify(JSON.parse(rawBody), null, 2);\n } catch {\n bodyText = rawBody;\n }\n } else {\n bodyText = rawBody;\n }\n const normalized = bodyText.slice(0, maxChars);\n return {\n url: response.url || inputUrl,\n fetched_url: inputUrl,\n title,\n http_status: response.status,\n content_type: contentType || 'application/octet-stream',\n excerpt: previewText(normalized, 320),\n body_text: normalized,\n text_length: bodyText.length,\n truncated: bodyText.length > normalized.length,\n headers,\n };\n };\n\n const classifyInboundEvent = (eventArgs) => {\n const source = trimText(eventArgs.source || 'event');\n const type = trimText(eventArgs.type || 'event');\n const summary = previewText(eventArgs.summary || eventArgs.message || (isPlainObject(eventArgs.event) ? JSON.stringify(eventArgs.event) : ''), 280);\n const severity = trimText(eventArgs.severity || '').toLowerCase();\n const priority = trimText(eventArgs.priority || '').toLowerCase();\n const extraText = isPlainObject(eventArgs.event) ? JSON.stringify(eventArgs.event) : '';\n const haystack = `${summary} ${extraText}`.toLowerCase();\n let classification = 'watch';\n let shouldNotify = false;\n\n if (\n ['critical', 'urgent', 'sev1', 'sev2', 'high'].includes(severity) ||\n ['critical', 'urgent', 'p1', 'p2', 'high'].includes(priority) ||\n /(outage|down|failed|failure|critical|urgent|blocked|sev1|sev2|database unavailable|cannot reach|immediately|asap)/.test(haystack)\n ) {\n classification = 'urgent';\n shouldNotify = true;\n } else if (\n /(delivery|delivered|package|submission|alert|invoice|payment|review requested|build failed|incident|ticket)/.test(haystack)\n ) {\n classification = 'important';\n shouldNotify = true;\n }\n\n return { source, type, summary, severity, priority, classification, shouldNotify };\n };\n\n let route = 'respond';\n let statusCode = 400;\n let responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'unknown_action', message: 'action is not supported' },\n };\n let notifyText = '';\n\n if (action === 'append_log') {\n if (typeof args.text === 'string' && trimText(args.text).length > 0) {\n statusCode = 200;\n const record = {\n ts: now,\n source: trimText(args.source || 'openclaw-action'),\n request_id: requestId,\n text: trimText(args.text),\n meta: isPlainObject(args.meta) ? args.meta : undefined,\n };\n persistActionLog(record);\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'append_log',\n status: 'logged',\n preview: { text: record.text },\n sink: {\n type: 'workflow-static-data',\n key: 'actionLog',\n retained_entries: maxLogEntries,\n },\n },\n };\n } else {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'invalid_request', message: 'required args are missing' },\n };\n }\n } else if (action === 'get_logs') {\n const actionLog = getActionLog();\n const limit = clamp(args.limit, 1, 50, 20);\n const entries = actionLog.slice(-limit).reverse();\n statusCode = 200;\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'get_logs',\n status: 'ok',\n count: entries.length,\n total_retained: actionLog.length,\n retained_entries: maxLogEntries,\n entries,\n },\n };\n } else if (action === 'send_notification_draft') {\n const title = trimText(args.title || '');\n const message = trimText(args.message || '');\n if (message) {\n statusCode = 200;\n const item = enqueueApproval({\n kind: 'notification',\n summary: title ? `Notification draft \u2014 ${title}` : `Notification draft \u2014 ${previewText(message, 80)}`,\n payload: {\n title,\n message,\n },\n tags: ['notify', 'approval'],\n });\n logAction(`queued notification draft ${item.id}`, { pending_id: item.id, title_present: Boolean(title) });\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'send_notification_draft',\n status: 'queued_for_approval',\n pending_id: item.id,\n approval_status: item.status,\n approval: makeApprovalPolicy('high', 'notification'),\n preview: {\n title,\n message,\n },\n sink: {\n type: 'workflow-static-data',\n key: 'approvalQueue',\n retained_entries: maxQueueEntries,\n },\n },\n };\n } else {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'invalid_request', message: 'send_notification_draft requires message' },\n };\n }\n } else if (action === 'notify') {\n if (typeof args.message === 'string' && trimText(args.message).length > 0) {\n route = 'notify';\n statusCode = 200;\n const title = trimText(args.title || '');\n notifyText = title ? `\ud83d\udd14 ${title}\\n${trimText(args.message)}` : `\ud83d\udd14 ${trimText(args.message)}`;\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'notify',\n status: 'sent',\n preview: { title, message: trimText(args.message) },\n targets: ['telegram', 'discord'],\n },\n };\n logAction(`notify: ${previewText(`${title} ${args.message}`, 180)}`);\n } else {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'invalid_request', message: 'required args are missing' },\n };\n }\n } else if (action === 'send_email_draft') {\n const to = ensureStringArray(args.to);\n const cc = ensureStringArray(args.cc);\n const bcc = ensureStringArray(args.bcc);\n const subject = trimText(args.subject || '');\n const bodyText = trimText(args.body_text || '');\n const bodyHtml = trimText(args.body_html || '');\n\n if (to.length > 0 && subject && (bodyText || bodyHtml)) {\n statusCode = 200;\n const item = enqueueApproval({\n kind: 'email_draft',\n summary: `Email draft to ${to.join(', ')} \u2014 ${subject}`,\n payload: {\n to,\n cc,\n bcc,\n subject,\n body_text: bodyText,\n body_html: bodyHtml,\n },\n tags: ['email', 'approval'],\n });\n logAction(`queued email draft ${item.id}`, { pending_id: item.id, subject, to_count: to.length });\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'send_email_draft',\n status: 'queued_for_approval',\n pending_id: item.id,\n approval_status: item.status,\n approval: makeApprovalPolicy('high', 'gmail'),\n preview: {\n to,\n cc,\n bcc,\n subject,\n body_excerpt: previewText(bodyText || stripHtml(bodyHtml), 240),\n },\n sink: {\n type: 'workflow-static-data',\n key: 'approvalQueue',\n retained_entries: maxQueueEntries,\n },\n },\n };\n } else {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: {\n code: 'invalid_request',\n message: 'send_email_draft requires to, subject, and body_text or body_html',\n },\n };\n }\n } else if (action === 'list_email_drafts') {\n const max = clamp(args.max, 1, 100, 20);\n const page = trimText(args.page || '');\n const all = args.all === true;\n const failEmpty = args.fail_empty === true;\n\n statusCode = 200;\n const item = enqueueApproval({\n kind: 'email_list_drafts',\n summary: all ? 'List Gmail drafts (all pages)' : `List Gmail drafts (max ${max})`,\n payload: {\n max,\n page,\n all,\n fail_empty: failEmpty,\n },\n tags: ['email', 'read', 'approval'],\n });\n logAction(`queued list_email_drafts ${item.id}`, { pending_id: item.id, max, all });\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'list_email_drafts',\n status: 'queued_for_approval',\n pending_id: item.id,\n approval_status: item.status,\n approval: makeApprovalPolicy('low', 'gmail'),\n preview: {\n max,\n page,\n all,\n fail_empty: failEmpty,\n },\n sink: {\n type: 'workflow-static-data',\n key: 'approvalQueue',\n retained_entries: maxQueueEntries,\n },\n },\n };\n } else if (action === 'delete_email_draft') {\n const draftId = trimText(args.draft_id || args.id || '');\n\n if (draftId) {\n statusCode = 200;\n const item = enqueueApproval({\n kind: 'email_draft_delete',\n summary: `Delete Gmail draft ${draftId}`,\n payload: {\n draft_id: draftId,\n },\n tags: ['email', 'approval', 'mutating', 'destructive'],\n });\n logAction(`queued delete_email_draft ${item.id}`, { pending_id: item.id, draft_id: draftId });\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'delete_email_draft',\n status: 'queued_for_approval',\n pending_id: item.id,\n approval_status: item.status,\n approval: makeApprovalPolicy('high', 'gmail'),\n preview: {\n draft_id: draftId,\n },\n sink: {\n type: 'workflow-static-data',\n key: 'approvalQueue',\n retained_entries: maxQueueEntries,\n },\n },\n };\n } else {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: {\n code: 'invalid_request',\n message: 'delete_email_draft requires draft_id',\n },\n };\n }\n } else if (action === 'send_gmail_draft' || action === 'send_approved_email') {\n const requestedAction = action;\n const draftId = trimText(args.draft_id || args.id || '');\n\n if (draftId) {\n statusCode = 200;\n const item = enqueueApproval({\n kind: 'email_draft_send',\n summary: `Send Gmail draft ${draftId}`,\n payload: {\n draft_id: draftId,\n requested_action: requestedAction,\n },\n tags: ['email', 'approval', 'mutating'],\n });\n logAction(`queued ${requestedAction} ${item.id}`, { pending_id: item.id, draft_id: draftId });\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'send_gmail_draft',\n requested_action: requestedAction,\n status: 'queued_for_approval',\n pending_id: item.id,\n approval_status: item.status,\n approval: makeApprovalPolicy('high', 'gmail'),\n preview: {\n draft_id: draftId,\n },\n sink: {\n type: 'workflow-static-data',\n key: 'approvalQueue',\n retained_entries: maxQueueEntries,\n },\n },\n };\n } else {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: {\n code: 'invalid_request',\n message: 'send_gmail_draft requires draft_id',\n },\n };\n }\n } else if (action === 'create_calendar_event') {\n const title = trimText(args.title || '');\n const start = trimText(args.start || '');\n const end = trimText(args.end || '');\n const startDate = start ? new Date(start) : null;\n const endDate = end ? new Date(end) : null;\n\n if (title && startDate && endDate && Number.isFinite(startDate.getTime()) && Number.isFinite(endDate.getTime()) && endDate > startDate) {\n statusCode = 200;\n const attendees = ensureStringArray(args.attendees);\n const item = enqueueApproval({\n kind: 'calendar_event',\n summary: `Calendar event ${title} @ ${startDate.toISOString()}`,\n payload: {\n calendar: trimText(args.calendar || 'primary') || 'primary',\n title,\n start: startDate.toISOString(),\n end: endDate.toISOString(),\n location: trimText(args.location || ''),\n description: trimText(args.description || ''),\n attendees,\n },\n tags: ['calendar', 'approval'],\n });\n logAction(`queued calendar event ${item.id}`, { pending_id: item.id, title, attendee_count: attendees.length });\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'create_calendar_event',\n status: 'queued_for_approval',\n pending_id: item.id,\n approval_status: item.status,\n approval: makeApprovalPolicy('high', 'calendar'),\n preview: {\n calendar: item.payload.calendar,\n title,\n start: item.payload.start,\n end: item.payload.end,\n location: item.payload.location,\n attendees,\n },\n sink: {\n type: 'workflow-static-data',\n key: 'approvalQueue',\n retained_entries: maxQueueEntries,\n },\n },\n };\n } else {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: {\n code: 'invalid_request',\n message: 'create_calendar_event requires title, start, and end with end after start',\n },\n };\n }\n } else if (action === 'list_upcoming_events') {\n const calendar = trimText(args.calendar || 'primary') || 'primary';\n const max = clamp(args.max, 1, 100, 20);\n const days = clamp(args.days, 1, 90, 7);\n const from = trimText(args.from || '');\n const to = trimText(args.to || '');\n const query = trimText(args.query || '');\n const allPages = args.all_pages === true;\n const failEmpty = args.fail_empty === true;\n\n let normalizedFrom = '';\n let normalizedTo = '';\n let validationError = '';\n\n if (from) {\n const parsedFrom = new Date(from);\n if (!Number.isFinite(parsedFrom.getTime())) {\n validationError = 'list_upcoming_events received an invalid from timestamp';\n } else {\n normalizedFrom = parsedFrom.toISOString();\n }\n }\n\n if (!validationError && to) {\n const parsedTo = new Date(to);\n if (!Number.isFinite(parsedTo.getTime())) {\n validationError = 'list_upcoming_events received an invalid to timestamp';\n } else {\n normalizedTo = parsedTo.toISOString();\n }\n }\n\n if (!validationError && normalizedFrom && normalizedTo && new Date(normalizedTo) <= new Date(normalizedFrom)) {\n validationError = 'list_upcoming_events requires to after from when both are provided';\n }\n\n if (validationError) {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: {\n code: 'invalid_request',\n message: validationError,\n },\n };\n } else {\n statusCode = 200;\n const windowSummary = normalizedFrom && normalizedTo\n ? `${normalizedFrom} \u2192 ${normalizedTo}`\n : normalizedFrom\n ? `from ${normalizedFrom}`\n : normalizedTo\n ? `until ${normalizedTo}`\n : `next ${days} days`;\n const item = enqueueApproval({\n kind: 'calendar_list_events',\n summary: `List upcoming events on ${calendar} (${windowSummary}, max ${max})`,\n payload: {\n calendar,\n max,\n days,\n from: normalizedFrom,\n to: normalizedTo,\n query,\n all_pages: allPages,\n fail_empty: failEmpty,\n },\n tags: ['calendar', 'read', 'approval'],\n });\n logAction(`queued list_upcoming_events ${item.id}`, { pending_id: item.id, calendar, max, days, has_from: Boolean(normalizedFrom), has_to: Boolean(normalizedTo) });\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'list_upcoming_events',\n status: 'queued_for_approval',\n pending_id: item.id,\n approval_status: item.status,\n approval: makeApprovalPolicy('low', 'calendar'),\n preview: {\n calendar,\n max,\n days,\n from: normalizedFrom,\n to: normalizedTo,\n query,\n all_pages: allPages,\n fail_empty: failEmpty,\n },\n sink: {\n type: 'workflow-static-data',\n key: 'approvalQueue',\n retained_entries: maxQueueEntries,\n },\n },\n };\n }\n } else if (action === 'update_calendar_event') {\n const calendar = trimText(args.calendar || 'primary') || 'primary';\n const eventId = trimText(args.event_id || args.id || '');\n const title = trimText(args.title || '');\n const location = trimText(args.location || '');\n const description = trimText(args.description || '');\n const startInput = trimText(args.start || '');\n const endInput = trimText(args.end || '');\n const attendeesProvided = Array.isArray(args.attendees) || typeof args.attendees === 'string';\n const attendees = attendeesProvided ? ensureStringArray(args.attendees) : [];\n const sendUpdatesRaw = trimText(args.send_updates || '').toLowerCase();\n const sendUpdates = sendUpdatesRaw === 'all'\n ? 'all'\n : sendUpdatesRaw === 'externalonly'\n ? 'externalOnly'\n : sendUpdatesRaw === 'none'\n ? 'none'\n : 'none';\n\n let normalizedStart = '';\n let normalizedEnd = '';\n let validationError = '';\n\n if (startInput) {\n const parsedStart = new Date(startInput);\n if (!Number.isFinite(parsedStart.getTime())) {\n validationError = 'update_calendar_event received an invalid start timestamp';\n } else {\n normalizedStart = parsedStart.toISOString();\n }\n }\n\n if (!validationError && endInput) {\n const parsedEnd = new Date(endInput);\n if (!Number.isFinite(parsedEnd.getTime())) {\n validationError = 'update_calendar_event received an invalid end timestamp';\n } else {\n normalizedEnd = parsedEnd.toISOString();\n }\n }\n\n if (!validationError && normalizedStart && normalizedEnd && new Date(normalizedEnd) <= new Date(normalizedStart)) {\n validationError = 'update_calendar_event requires end after start when both are provided';\n }\n\n const payload = {\n calendar,\n event_id: eventId,\n send_updates: sendUpdates,\n };\n let patchCount = 0;\n if (title) {\n payload.title = title;\n patchCount += 1;\n }\n if (normalizedStart) {\n payload.start = normalizedStart;\n patchCount += 1;\n }\n if (normalizedEnd) {\n payload.end = normalizedEnd;\n patchCount += 1;\n }\n if (description) {\n payload.description = description;\n patchCount += 1;\n }\n if (location) {\n payload.location = location;\n patchCount += 1;\n }\n if (attendeesProvided) {\n payload.attendees = attendees;\n patchCount += 1;\n }\n\n if (validationError) {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: {\n code: 'invalid_request',\n message: validationError,\n },\n };\n } else if (!eventId || patchCount === 0) {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: {\n code: 'invalid_request',\n message: 'update_calendar_event requires event_id and at least one of title, start, end, description, location, or attendees',\n },\n };\n } else {\n statusCode = 200;\n const item = enqueueApproval({\n kind: 'calendar_event_update',\n summary: title ? `Update calendar event ${eventId} \u2014 ${title}` : `Update calendar event ${eventId}`,\n payload,\n tags: ['calendar', 'approval', 'mutating'],\n });\n logAction(`queued update_calendar_event ${item.id}`, { pending_id: item.id, event_id: eventId, patch_count: patchCount });\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'update_calendar_event',\n status: 'queued_for_approval',\n pending_id: item.id,\n approval_status: item.status,\n approval: makeApprovalPolicy('high', 'calendar'),\n preview: payload,\n sink: {\n type: 'workflow-static-data',\n key: 'approvalQueue',\n retained_entries: maxQueueEntries,\n },\n },\n };\n }\n } else if (action === 'delete_calendar_event') {\n const calendar = trimText(args.calendar || 'primary') || 'primary';\n const eventId = trimText(args.event_id || args.id || '');\n const sendUpdatesRaw = trimText(args.send_updates || '').toLowerCase();\n const sendUpdates = sendUpdatesRaw === 'all'\n ? 'all'\n : sendUpdatesRaw === 'externalonly'\n ? 'externalOnly'\n : sendUpdatesRaw === 'none'\n ? 'none'\n : 'none';\n\n if (eventId) {\n statusCode = 200;\n const item = enqueueApproval({\n kind: 'calendar_event_delete',\n summary: `Delete calendar event ${eventId}`,\n payload: {\n calendar,\n event_id: eventId,\n send_updates: sendUpdates,\n },\n tags: ['calendar', 'approval', 'mutating', 'destructive'],\n });\n logAction(`queued delete_calendar_event ${item.id}`, { pending_id: item.id, event_id: eventId });\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'delete_calendar_event',\n status: 'queued_for_approval',\n pending_id: item.id,\n approval_status: item.status,\n approval: makeApprovalPolicy('high', 'calendar'),\n preview: {\n calendar,\n event_id: eventId,\n send_updates: sendUpdates,\n },\n sink: {\n type: 'workflow-static-data',\n key: 'approvalQueue',\n retained_entries: maxQueueEntries,\n },\n },\n };\n } else {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: {\n code: 'invalid_request',\n message: 'delete_calendar_event requires event_id',\n },\n };\n }\n } else if (action === 'approval_queue_add') {\n const kind = trimText(args.kind || 'manual');\n const summary = trimText(args.summary || '');\n if (summary) {\n statusCode = 200;\n const item = enqueueApproval({\n kind,\n summary,\n payload: isPlainObject(args.payload) ? args.payload : {},\n tags: ensureStringArray(args.tags),\n });\n logAction(`queued approval item ${item.id}`, { pending_id: item.id, kind });\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'approval_queue_add',\n status: 'queued',\n pending_id: item.id,\n item,\n },\n };\n } else {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'invalid_request', message: 'approval_queue_add requires summary' },\n };\n }\n } else if (action === 'approval_queue_list') {\n const limit = clamp(args.limit, 1, 50, 20);\n const includeHistory = args.include_history === true;\n const pending = getApprovalQueue().slice(-limit).reverse();\n const history = includeHistory ? getApprovalHistory().slice(-limit).reverse() : [];\n const pending_compact = pending.map((item) => compactApprovalItem(item));\n const history_compact = history.map((item) => compactApprovalItem(item));\n statusCode = 200;\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'approval_queue_list',\n status: 'ok',\n pending_count: getApprovalQueue().length,\n history_count: getApprovalHistory().length,\n pending,\n pending_compact,\n history,\n history_compact,\n },\n };\n } else if (action === 'approval_queue_resolve') {\n const id = trimText(args.id || '');\n const decision = trimText(args.decision || '').toLowerCase();\n const queue = getApprovalQueue();\n const index = queue.findIndex((item) => item && item.id === id);\n\n if (!id || !['approve', 'reject'].includes(decision)) {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'invalid_request', message: 'approval_queue_resolve requires id and decision=approve|reject' },\n };\n } else if (index === -1) {\n statusCode = 404;\n responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'not_found', message: 'approval queue item was not found' },\n };\n } else {\n statusCode = 200;\n const [item] = queue.splice(index, 1);\n workflowStaticData.approvalQueue = queue;\n const resolved = {\n ...item,\n status: decision === 'approve' ? 'approved' : 'rejected',\n updated_at: now,\n resolved_at: now,\n resolution_note: trimText(args.note || ''),\n };\n\n let executed = false;\n let executed_action = '';\n if (decision === 'approve' && resolved.kind === 'notification' && isPlainObject(resolved.payload)) {\n const title = trimText(resolved.payload.title || '');\n const message = trimText(resolved.payload.message || '');\n if (message) {\n route = 'notify';\n notifyText = title ? `\ud83d\udd14 ${title}\n${message}` : `\ud83d\udd14 ${message}`;\n executed = true;\n executed_action = 'notify';\n resolved.execution = {\n action: 'notify',\n status: 'sent',\n executed_at: now,\n summary: title ? `notify sent (${title})` : 'notify sent',\n result_refs: {},\n };\n }\n }\n\n resolved.operator = buildOperatorView(resolved);\n workflowStaticData.approvalHistory = pushRetained(getApprovalHistory(), resolved, maxHistoryEntries);\n logAction(`resolved approval item ${id}`, { pending_id: id, status: resolved.status, executed, executed_action, summary_line: resolved.operator.summary_line });\n if (!executed && args.notify_on_resolve === true) {\n route = 'notify';\n const emoji = resolved.status === 'approved' ? '\u2705' : '\ud83d\uded1';\n notifyText = `${emoji} Approval ${resolved.status}\n${resolved.summary}`;\n }\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'approval_queue_resolve',\n status: resolved.status,\n executed,\n executed_action,\n item: resolved,\n item_compact: compactApprovalItem(resolved),\n },\n };\n }\n } else if (action === 'approval_history_attach_execution') {\n const id = trimText(args.id || '');\n const execution = isPlainObject(args.execution) ? args.execution : null;\n const history = getApprovalHistory();\n const index = history.findIndex((item) => item && item.id === id);\n\n if (!id || !execution) {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'invalid_request', message: 'approval_history_attach_execution requires id and execution object' },\n };\n } else if (index === -1) {\n statusCode = 404;\n responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'not_found', message: 'approval history item was not found' },\n };\n } else {\n statusCode = 200;\n const existing = history[index];\n const mergedExecution = {\n ...(isPlainObject(existing.execution) ? existing.execution : {}),\n ...execution,\n updated_at: now,\n };\n if (!isPlainObject(mergedExecution.result_refs)) {\n mergedExecution.result_refs = extractExecutionRefs(mergedExecution);\n }\n const updated = {\n ...existing,\n updated_at: now,\n execution: mergedExecution,\n };\n updated.operator = buildOperatorView(updated);\n history[index] = updated;\n workflowStaticData.approvalHistory = history;\n logAction(`attached execution for approval item ${id}`, { pending_id: id, execution_status: updated.execution.status || '', summary_line: updated.operator.summary_line });\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'approval_history_attach_execution',\n status: 'updated',\n item: updated,\n item_compact: compactApprovalItem(updated),\n },\n };\n }\n } else if (action === 'fetch_and_normalize_url') {\n const url = trimText(args.url || '');\n const maxChars = clamp(args.max_chars, 500, 20000, 8000);\n const timeoutMs = clamp(args.timeout_ms, 1000, 30000, 10000);\n\n if (!/^https?:\\/\\//i.test(url)) {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'invalid_request', message: 'fetch_and_normalize_url requires an http or https url' },\n };\n } else {\n try {\n if (!this || !this.helpers || typeof this.helpers.httpRequest !== 'function') {\n throw new Error('this.helpers.httpRequest is not available in this n8n Code runtime');\n }\n const extraHeaders = isPlainObject(args.headers) ? args.headers : {};\n const requestHeaders = Object.fromEntries(\n Object.entries(extraHeaders)\n .filter(([key, value]) => typeof key === 'string' && typeof value === 'string' && key.trim() && value.trim())\n .map(([key, value]) => [key.trim(), value.trim()])\n );\n\n const skipSslCertificateValidation = args.skip_ssl_certificate_validation === true;\n const upstream = await this.helpers.httpRequest({\n url,\n method: 'GET',\n headers: requestHeaders,\n timeout: timeoutMs,\n json: false,\n encoding: 'text',\n returnFullResponse: true,\n skipSslCertificateValidation,\n });\n\n const headersObj = Object.fromEntries(\n Object.entries(upstream.headers || {}).map(([key, value]) => [\n String(key).toLowerCase(),\n Array.isArray(value) ? value.join(', ') : String(value ?? ''),\n ])\n );\n const response = {\n ok: (upstream.statusCode || 0) >= 200 && (upstream.statusCode || 0) < 300,\n status: upstream.statusCode || 0,\n url: upstream.request?.uri?.href || upstream.request?.href || upstream.url || url,\n headers: { entries: () => Object.entries(headersObj) },\n };\n const rawBody = typeof upstream.body === 'string'\n ? upstream.body\n : JSON.stringify(upstream.body ?? '', null, 2);\n const normalized = normalizeFetchResponse({ inputUrl: url, response, rawBody, maxChars });\n if (!response.ok) {\n statusCode = 502;\n responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'upstream_http_error', message: `upstream returned ${response.status}` },\n result: {\n action: 'fetch_and_normalize_url',\n upstream: normalized,\n },\n };\n } else {\n statusCode = 200;\n logAction(`fetched url ${normalized.url}`, { url: normalized.url, content_type: normalized.content_type, http_status: normalized.http_status });\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'fetch_and_normalize_url',\n status: 'ok',\n ...normalized,\n },\n };\n }\n } catch (error) {\n statusCode = 502;\n responseBody = {\n ok: false,\n request_id: requestId,\n error: {\n code: error && error.name === 'AbortError' ? 'upstream_timeout' : 'fetch_failed',\n message: error && error.message ? String(error.message) : 'fetch failed',\n },\n };\n }\n }\n } else if (action === 'inbound_event_filter') {\n const classified = classifyInboundEvent(args);\n if (!classified.summary) {\n responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'invalid_request', message: 'inbound_event_filter requires summary or message' },\n };\n } else {\n const dedupeKey = trimText(args.dedupe_key || `${classified.source}|${classified.type}|${classified.summary}`.toLowerCase().replace(/\\s+/g, ' '));\n const dedupList = getEventDedup();\n const duplicate = dedupList.includes(dedupeKey);\n if (!duplicate) {\n workflowStaticData.eventDedup = pushRetained(dedupList, dedupeKey, maxDedupEntries);\n }\n const eventRecord = {\n ts: now,\n source: classified.source,\n type: classified.type,\n summary: classified.summary,\n severity: classified.severity,\n priority: classified.priority,\n classification: duplicate ? 'deduped' : classified.classification,\n dedupe_key: dedupeKey,\n duplicate,\n event: isPlainObject(args.event) ? args.event : undefined,\n };\n workflowStaticData.inboundEvents = pushRetained(getInboundEvents(), eventRecord, maxEventEntries);\n const notifyAllowed = args.notify !== false;\n const shouldNotify = classified.shouldNotify && !duplicate && notifyAllowed;\n if (shouldNotify) {\n route = 'notify';\n const emoji = classified.classification === 'urgent' ? '\ud83d\udea8' : '\u2139\ufe0f';\n notifyText = `${emoji} ${classified.source}: ${classified.summary}`;\n }\n logAction(`ingested inbound event ${classified.source}`, { classification: eventRecord.classification, duplicate });\n statusCode = 200;\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'inbound_event_filter',\n status: duplicate ? 'deduped' : 'stored',\n classification: eventRecord.classification,\n duplicate,\n notified: shouldNotify,\n event: eventRecord,\n sink: {\n type: 'workflow-static-data',\n key: 'inboundEvents',\n retained_entries: maxEventEntries,\n },\n },\n };\n }\n }\n\n return {\n json: {\n route,\n status_code: statusCode,\n response_body: responseBody,\n notify_text: notifyText,\n },\n };\n})();\n"
}
},
{
"id": "route-dispatch",
"name": "route-dispatch",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.4,
"position": [
-120,
40
],
"parameters": {
"mode": "rules",
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"leftValue": "={{$json.route}}",
"rightValue": "notify",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "notify"
}
]
},
"options": {
"fallbackOutput": "extra",
"renameFallbackOutput": "respond"
}
}
},
{
"id": "send-telegram-notification",
"name": "Send Telegram Notification",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
160,
40
],
"parameters": {
"chatId": "8367012007",
"text": "={{$json.notify_text}}",
"additionalFields": {}
},
"credentials": {
"telegramApi": {
"id": "aox4dyIWVSRdcH5z",
"name": "Telegram Bot (OpenClaw)"
}
}
},
{
"id": "send-discord-notification",
"name": "Send Discord Notification",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
460,
40
],
"parameters": {
"authentication": "predefinedCredentialType",
"nodeCredentialType": "httpHeaderAuth",
"method": "POST",
"url": "https://discord.com/api/v10/channels/425781661268049931/messages",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ { content: $node[\"route-action\"].json[\"notify_text\"] } }}",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"id": "UgPqYcoCNNIgr55m",
"name": "Discord Bot Auth"
}
}
},
{
"id": "respond-openclaw-action",
"name": "Respond to Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
760,
40
],
"parameters": {
"respondWith": "json",
"responseBody": "={{$node[\"route-action\"].json[\"response_body\"]}}",
"options": {
"responseCode": "={{$node[\"route-action\"].json[\"status_code\"]}}"
}
}
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "route-action",
"type": "main",
"index": 0
}
]
]
},
"route-action": {
"main": [
[
{
"node": "route-dispatch",
"type": "main",
"index": 0
}
]
]
},
"route-dispatch": {
"main": [
[
{
"node": "Send Telegram Notification",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
},
"Send Telegram Notification": {
"main": [
[
{
"node": "Send Discord Notification",
"type": "main",
"index": 0
}
]
]
},
"Send Discord Notification": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"meta": {
"templateCredsSetupCompleted": false,
"note": "After import, set Webhook authentication to Header Auth and bind a local credential using x-openclaw-secret. This asset ships append_log + get_logs via workflow static data plus Telegram/Discord notify fan-out."
},
"active": false,
"versionId": "openclaw-action-v8"
}