From 9b8b744cb1abc8ffe1e08ccd26365cff6c0c777b Mon Sep 17 00:00:00 2001 From: zap Date: Thu, 12 Mar 2026 07:32:40 +0000 Subject: [PATCH] fix(skill): simplify n8n action router --- memory/2026-03-12.md | 11 + .../assets/openclaw-action.workflow.json | 305 +----------------- .../n8n-webhook/scripts/validate-workflow.py | 57 ++-- 3 files changed, 43 insertions(+), 330 deletions(-) diff --git a/memory/2026-03-12.md b/memory/2026-03-12.md index 8394dec..688dd98 100644 --- a/memory/2026-03-12.md +++ b/memory/2026-03-12.md @@ -11,3 +11,14 @@ - Will clarified the primary host LAN IP to use/document is `192.168.153.113`. - Finished local skill `skills/n8n-webhook` for authenticated webhook-first n8n integration, including `scripts/call-webhook.sh`, `scripts/call-action.sh`, `scripts/validate-workflow.py`, an importable `assets/openclaw-action.workflow.json`, sample payloads, payload notes, and a successful package/validation run to `/tmp/n8n-skill-dist/n8n-webhook.skill`. - The shipped `openclaw-action` workflow intentionally leaves Webhook authentication unset in export JSON; after import, bind local n8n Header Auth credentials manually using `x-openclaw-secret` so secrets are not embedded in the skill asset. +- Live n8n API access was confirmed and used on 2026-03-12 against `http://192.168.153.113:18808` (public API + existing webhook credential available in the instance). +- Created and activated live workflow `openclaw-action` via the n8n API. +- First live implementation matched the original asset shape (`Webhook -> Set -> Switch -> Respond`) but failed at runtime: executions errored in the `normalize-request` Set node with `invalid syntax` on its expressions. +- Fix: replaced the live router logic and shipped asset implementation with a simpler, working internal design: `Webhook -> Code -> Respond to Webhook`, while preserving the external contract (`append_log`, `notify`, normalized JSON success/failure responses). +- Important operational note: the workflow initially activated without a usable production route because the Webhook node lacked a `webhookId`; adding one and re-publishing was necessary for proper webhook registration. +- Current state before compaction: the live `openclaw-action` workflow exists in n8n, is active, and has been updated to the simpler Code-node implementation; post-update live response testing was still in progress at compaction time. +- After compaction, live verification succeeded against the production webhook: + - `append_log` returned `200` with normalized JSON success payload + - `notify` returned `200` with normalized JSON success payload + - unknown action returned `400` with `{ code: "unknown_action" }` +- The packaged skill artifact was refreshed after the router simplification at `/tmp/n8n-skill-dist/n8n-webhook.skill`. diff --git a/skills/n8n-webhook/assets/openclaw-action.workflow.json b/skills/n8n-webhook/assets/openclaw-action.workflow.json index 54b4006..f2ee1fc 100644 --- a/skills/n8n-webhook/assets/openclaw-action.workflow.json +++ b/skills/n8n-webhook/assets/openclaw-action.workflow.json @@ -7,7 +7,7 @@ "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ - -820, + -360, 0 ], "parameters": { @@ -18,252 +18,30 @@ "options": {} } }, - { - "id": "normalize-request", - "name": "normalize-request", - "type": "n8n-nodes-base.set", - "typeVersion": 3.4, - "position": [ - -560, - 0 - ], - "parameters": { - "mode": "manual", - "includeOtherFields": false, - "assignments": { - "assignments": [ - { - "id": "action", - "name": "action", - "type": "string", - "value": "={{$json.body.action || ''}}" - }, - { - "id": "args", - "name": "args", - "type": "object", - "value": "={{$json.body.args || {}}}" - }, - { - "id": "request_id", - "name": "request_id", - "type": "string", - "value": "={{$json.body.request_id || ''}}" - } - ] - }, - "options": { - "dotNotation": false - } - } - }, { "id": "route-action", "name": "route-action", - "type": "n8n-nodes-base.switch", - "typeVersion": 3.4, + "type": "n8n-nodes-base.code", + "typeVersion": 2, "position": [ - -300, + -40, 0 ], "parameters": { - "mode": "rules", - "rules": { - "values": [ - { - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "strict", - "version": 2 - }, - "conditions": [ - { - "leftValue": "={{$json.action}}", - "rightValue": "append_log", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "append_log" - }, - { - "conditions": { - "options": { - "caseSensitive": true, - "typeValidation": "strict", - "version": 2 - }, - "conditions": [ - { - "leftValue": "={{$json.action}}", - "rightValue": "notify", - "operator": { - "type": "string", - "operation": "equals" - } - } - ], - "combinator": "and" - }, - "renameOutput": true, - "outputKey": "notify" - } - ] - }, - "options": { - "fallbackOutput": "extra", - "renameFallbackOutput": "unknown" - } + "mode": "runOnceForEachItem", + "language": "javaScript", + "jsCode": "const body = $json.body ?? {};\nconst action = body.action ?? '';\nconst args = body.args ?? {};\nconst requestId = body.request_id ?? '';\n\nlet statusCode = 200;\nlet responseBody;\n\nif (action === 'append_log') {\n if (typeof args.text === 'string' && args.text.length > 0) {\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'append_log',\n status: 'accepted',\n preview: { text: args.text },\n },\n };\n } else {\n statusCode = 400;\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 === 'notify') {\n if (typeof args.message === 'string' && args.message.length > 0) {\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'notify',\n status: 'accepted',\n preview: {\n title: typeof args.title === 'string' ? args.title : '',\n message: args.message,\n },\n },\n };\n } else {\n statusCode = 400;\n responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'invalid_request', message: 'required args are missing' },\n };\n }\n} else {\n statusCode = 400;\n responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'unknown_action', message: 'action is not supported' },\n };\n}\n\nreturn {\n json: {\n status_code: statusCode,\n response_body: responseBody,\n },\n};" } }, { - "id": "append-log-response", - "name": "append-log-response", - "type": "n8n-nodes-base.set", - "typeVersion": 3.4, - "position": [ - -20, - -180 - ], - "parameters": { - "mode": "manual", - "includeOtherFields": false, - "assignments": { - "assignments": [ - { - "id": "status_code", - "name": "status_code", - "type": "number", - "value": "={{$json.args.text ? 200 : 400}}" - }, - { - "id": "response_body", - "name": "response_body", - "type": "object", - "value": "={{ $json.args.text ? { ok: true, request_id: $json.request_id || '', result: { action: 'append_log', status: 'accepted', preview: { text: $json.args.text } } } : { ok: false, request_id: $json.request_id || '', error: { code: 'invalid_request', message: 'required args are missing' } } }}" - } - ] - }, - "options": { - "dotNotation": false - } - } - }, - { - "id": "respond-append-log", - "name": "respond-append-log", + "id": "respond-openclaw-action", + "name": "Respond to Webhook", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.5, "position": [ - 240, - -180 - ], - "parameters": { - "respondWith": "json", - "responseBody": "={{$json.response_body}}", - "options": { - "responseCode": "={{$json.status_code}}" - } - } - }, - { - "id": "notify-response", - "name": "notify-response", - "type": "n8n-nodes-base.set", - "typeVersion": 3.4, - "position": [ - -20, + 260, 0 ], - "parameters": { - "mode": "manual", - "includeOtherFields": false, - "assignments": { - "assignments": [ - { - "id": "status_code", - "name": "status_code", - "type": "number", - "value": "={{$json.args.message ? 200 : 400}}" - }, - { - "id": "response_body", - "name": "response_body", - "type": "object", - "value": "={{ $json.args.message ? { ok: true, request_id: $json.request_id || '', result: { action: 'notify', status: 'accepted', preview: { title: $json.args.title || '', message: $json.args.message } } } : { ok: false, request_id: $json.request_id || '', error: { code: 'invalid_request', message: 'required args are missing' } } }}" - } - ] - }, - "options": { - "dotNotation": false - } - } - }, - { - "id": "respond-notify", - "name": "respond-notify", - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1.5, - "position": [ - 240, - 0 - ], - "parameters": { - "respondWith": "json", - "responseBody": "={{$json.response_body}}", - "options": { - "responseCode": "={{$json.status_code}}" - } - } - }, - { - "id": "unknown-action-response", - "name": "unknown-action-response", - "type": "n8n-nodes-base.set", - "typeVersion": 3.4, - "position": [ - -20, - 180 - ], - "parameters": { - "mode": "manual", - "includeOtherFields": false, - "assignments": { - "assignments": [ - { - "id": "status_code", - "name": "status_code", - "type": "number", - "value": 400 - }, - { - "id": "response_body", - "name": "response_body", - "type": "object", - "value": "={{ { ok: false, request_id: $json.request_id || '', error: { code: 'unknown_action', message: 'action is not supported' } } }}" - } - ] - }, - "options": { - "dotNotation": false - } - } - }, - { - "id": "respond-unknown-action", - "name": "respond-unknown-action", - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1.5, - "position": [ - 240, - 180 - ], "parameters": { "respondWith": "json", "responseBody": "={{$json.response_body}}", @@ -275,17 +53,6 @@ ], "connections": { "Webhook": { - "main": [ - [ - { - "node": "normalize-request", - "type": "main", - "index": 0 - } - ] - ] - }, - "normalize-request": { "main": [ [ { @@ -300,54 +67,7 @@ "main": [ [ { - "node": "append-log-response", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "notify-response", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "unknown-action-response", - "type": "main", - "index": 0 - } - ] - ] - }, - "append-log-response": { - "main": [ - [ - { - "node": "respond-append-log", - "type": "main", - "index": 0 - } - ] - ] - }, - "notify-response": { - "main": [ - [ - { - "node": "respond-notify", - "type": "main", - "index": 0 - } - ] - ] - }, - "unknown-action-response": { - "main": [ - [ - { - "node": "respond-unknown-action", + "node": "Respond to Webhook", "type": "main", "index": 0 } @@ -355,7 +75,6 @@ ] } }, - "pinData": {}, "settings": { "executionOrder": "v1" }, @@ -365,5 +84,5 @@ "note": "After import, set Webhook authentication to Header Auth and bind a local credential using x-openclaw-secret. Secrets are intentionally not embedded in the workflow export." }, "active": false, - "versionId": "openclaw-action-v1" + "versionId": "openclaw-action-v2" } diff --git a/skills/n8n-webhook/scripts/validate-workflow.py b/skills/n8n-webhook/scripts/validate-workflow.py index 8501c45..de3f781 100755 --- a/skills/n8n-webhook/scripts/validate-workflow.py +++ b/skills/n8n-webhook/scripts/validate-workflow.py @@ -5,26 +5,14 @@ from pathlib import Path REQUIRED_NODE_NAMES = { 'Webhook', - 'normalize-request', 'route-action', - 'append-log-response', - 'respond-append-log', - 'notify-response', - 'respond-notify', - 'unknown-action-response', - 'respond-unknown-action', + 'Respond to Webhook', } EXPECTED_DIRECT_TYPES = { 'Webhook': 'n8n-nodes-base.webhook', - 'normalize-request': 'n8n-nodes-base.set', - 'route-action': 'n8n-nodes-base.switch', - 'append-log-response': 'n8n-nodes-base.set', - 'respond-append-log': 'n8n-nodes-base.respondToWebhook', - 'notify-response': 'n8n-nodes-base.set', - 'respond-notify': 'n8n-nodes-base.respondToWebhook', - 'unknown-action-response': 'n8n-nodes-base.set', - 'respond-unknown-action': 'n8n-nodes-base.respondToWebhook', + 'route-action': 'n8n-nodes-base.code', + 'Respond to Webhook': 'n8n-nodes-base.respondToWebhook', } @@ -81,30 +69,25 @@ def main(): if webhook_params.get('responseMode') != 'responseNode': fail('Webhook.responseMode must be responseNode') - normalize = by_name['normalize-request'].get('parameters', {}) - normalize_assignments = normalize.get('assignments', {}).get('assignments', []) - normalize_fields = {a.get('name') for a in normalize_assignments if isinstance(a, dict)} - for field in ('action', 'args', 'request_id'): - if field not in normalize_fields: - fail(f'normalize-request must assign {field!r}') - - route = by_name['route-action'].get('parameters', {}) - rule_values = route.get('rules', {}).get('values', []) - if len(rule_values) < 2: - fail('route-action must define at least two routing rules') - rule_names = {rule.get('outputKey') for rule in rule_values if isinstance(rule, dict)} - for action in ('append_log', 'notify'): - if action not in rule_names: - fail(f'route-action must have a routing rule for {action!r}') + router = by_name['route-action'].get('parameters', {}) + if router.get('mode') != 'runOnceForEachItem': + fail('route-action code node must use runOnceForEachItem mode') + if router.get('language') != 'javaScript': + fail('route-action code node must use javaScript language') + js_code = router.get('jsCode', '') + for snippet in ("append_log", "notify", "unknown_action", "invalid_request", "status_code", "response_body"): + if snippet not in js_code: + fail(f'route-action jsCode missing expected snippet: {snippet!r}') route_outputs = connections.get('route-action', {}).get('main', []) - if len(route_outputs) < 3: - fail('route-action must expose append_log, notify, and fallback outputs') + if len(route_outputs) < 1: + fail('route-action must connect to Respond to Webhook') - for responder_name in ('respond-append-log', 'respond-notify', 'respond-unknown-action'): - params = by_name[responder_name].get('parameters', {}) - if params.get('respondWith') != 'json': - fail(f'{responder_name} must respondWith json') + responder = by_name['Respond to Webhook'].get('parameters', {}) + if responder.get('respondWith') != 'json': + fail('Respond to Webhook must respondWith json') + if responder.get('responseBody') != '={{$json.response_body}}': + fail('Respond to Webhook must use $json.response_body as responseBody') sample_paths = [ path.parent / 'test-append-log.json', @@ -120,7 +103,7 @@ def main(): print('OK: workflow asset structure looks consistent') print(f'- workflow: {path}') print(f'- nodes: {len(nodes)}') - print(f'- rules: {len(rule_values)} + fallback') + print('- router: code node with append_log + notify + fallback') print('- samples: test-append-log.json, test-notify.json')