diff --git a/memory/2026-03-12.md b/memory/2026-03-12.md index cb6cb0f..72209ad 100644 --- a/memory/2026-03-12.md +++ b/memory/2026-03-12.md @@ -30,3 +30,12 @@ - Final fix: switched `append_log` to use n8n workflow static data (`$getWorkflowStaticData('global')`) under key `actionLog`, capped to the latest 200 entries. - Verified persisted state via the n8n API: `staticData.global.actionLog` contains the live test record for request `live-log-003`. - Conclusion: for small recent operational breadcrumbs, workflow static data is the right sink here; MinIO is better reserved for later archival/rotation/export use cases rather than tiny per-event appends. +- Added action `get_logs` to the live `openclaw-action` workflow and local `n8n-webhook` skill. + - `get_logs` reads from workflow static data key `actionLog` + - default limit `20`, clamped to `1..50`, newest-first + - verified live with request `live-getlogs-001` returning the seed record from `live-log-004` +- Re-verified the three live actions together after the update: + - `append_log` → success + - `get_logs` → success + - `notify` → success +- Refreshed packaged skill artifact again at `/tmp/n8n-skill-dist/n8n-webhook.skill`. diff --git a/skills/n8n-webhook/SKILL.md b/skills/n8n-webhook/SKILL.md index 9ea6d95..72b873c 100644 --- a/skills/n8n-webhook/SKILL.md +++ b/skills/n8n-webhook/SKILL.md @@ -51,6 +51,7 @@ Call the preferred action-bus route: ```bash scripts/call-action.sh append_log --args '{"text":"backup complete"}' --request-id auto +scripts/call-action.sh get_logs --args '{"limit":5}' --pretty ``` Call a test webhook while editing a flow: @@ -98,6 +99,7 @@ This keeps the external surface small while letting n8n route internally. Use the included workflow asset when you want a ready-made local router for: - `append_log` → append small records into workflow static data (`actionLog`, latest 200) +- `get_logs` → read the most recent retained records from `actionLog` - `notify` → send through the current Telegram + Discord notification paths - normalized JSON success/failure responses - unknown-action handling diff --git a/skills/n8n-webhook/assets/openclaw-action.workflow.json b/skills/n8n-webhook/assets/openclaw-action.workflow.json index 5e1c0ae..bb985b6 100644 --- a/skills/n8n-webhook/assets/openclaw-action.workflow.json +++ b/skills/n8n-webhook/assets/openclaw-action.workflow.json @@ -30,7 +30,7 @@ "parameters": { "mode": "runOnceForEachItem", "language": "javaScript", - "jsCode": "const body = $json.body ?? {};\nconst action = body.action ?? '';\nconst args = body.args ?? {};\nconst requestId = body.request_id ?? '';\nconst now = new Date().toISOString();\nconst workflowStaticData = $getWorkflowStaticData('global');\nconst maxLogEntries = 200;\n\nlet route = 'respond';\nlet statusCode = 400;\nlet responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'unknown_action', message: 'action is not supported' },\n};\nlet notifyText = '';\n\nif (action === 'append_log') {\n if (typeof args.text === 'string' && args.text.length > 0) {\n statusCode = 200;\n const record = {\n ts: now,\n source: 'openclaw-action',\n request_id: requestId,\n text: args.text,\n meta: typeof args.meta === 'object' && args.meta !== null ? args.meta : undefined,\n };\n const actionLog = Array.isArray(workflowStaticData.actionLog) ? workflowStaticData.actionLog : [];\n actionLog.push(record);\n if (actionLog.length > maxLogEntries) {\n actionLog.splice(0, actionLog.length - maxLogEntries);\n }\n workflowStaticData.actionLog = actionLog;\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'append_log',\n status: 'logged',\n preview: { text: args.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 === 'notify') {\n if (typeof args.message === 'string' && args.message.length > 0) {\n route = 'notify';\n statusCode = 200;\n const title = typeof args.title === 'string' ? args.title : '';\n notifyText = title ? `🔔 ${title}\\n${args.message}` : `🔔 ${args.message}`;\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'notify',\n status: 'sent',\n preview: { title, message: args.message },\n targets: ['telegram', 'discord'],\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}\n\nreturn {\n json: {\n route,\n status_code: statusCode,\n response_body: responseBody,\n notify_text: notifyText,\n },\n};" + "jsCode": "const body = $json.body ?? {};\nconst action = body.action ?? '';\nconst args = body.args ?? {};\nconst requestId = body.request_id ?? '';\nconst now = new Date().toISOString();\nconst workflowStaticData = $getWorkflowStaticData('global');\nconst maxLogEntries = 200;\n\nlet route = 'respond';\nlet statusCode = 400;\nlet responseBody = {\n ok: false,\n request_id: requestId,\n error: { code: 'unknown_action', message: 'action is not supported' },\n};\nlet notifyText = '';\n\nif (action === 'append_log') {\n if (typeof args.text === 'string' && args.text.length > 0) {\n statusCode = 200;\n const record = {\n ts: now,\n source: 'openclaw-action',\n request_id: requestId,\n text: args.text,\n meta: typeof args.meta === 'object' && args.meta !== null ? args.meta : undefined,\n };\n const actionLog = Array.isArray(workflowStaticData.actionLog) ? workflowStaticData.actionLog : [];\n actionLog.push(record);\n if (actionLog.length > maxLogEntries) {\n actionLog.splice(0, actionLog.length - maxLogEntries);\n }\n workflowStaticData.actionLog = actionLog;\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'append_log',\n status: 'logged',\n preview: { text: args.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 = Array.isArray(workflowStaticData.actionLog) ? workflowStaticData.actionLog : [];\n const rawLimit = Number.isFinite(Number(args.limit)) ? Number(args.limit) : 20;\n const limit = Math.max(1, Math.min(50, Math.trunc(rawLimit) || 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 === 'notify') {\n if (typeof args.message === 'string' && args.message.length > 0) {\n route = 'notify';\n statusCode = 200;\n const title = typeof args.title === 'string' ? args.title : '';\n notifyText = title ? `🔔 ${title}\\n${args.message}` : `🔔 ${args.message}`;\n responseBody = {\n ok: true,\n request_id: requestId,\n result: {\n action: 'notify',\n status: 'sent',\n preview: { title, message: args.message },\n targets: ['telegram', 'discord'],\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}\n\nreturn {\n json: {\n route,\n status_code: statusCode,\n response_body: responseBody,\n notify_text: notifyText,\n },\n};" } }, { @@ -211,8 +211,8 @@ "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 real append_log persistence via workflow static data plus Telegram/Discord notify fan-out." + "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-v5" + "versionId": "openclaw-action-v6" } diff --git a/skills/n8n-webhook/references/openclaw-action.md b/skills/n8n-webhook/references/openclaw-action.md index 2b904a6..052273d 100644 --- a/skills/n8n-webhook/references/openclaw-action.md +++ b/skills/n8n-webhook/references/openclaw-action.md @@ -10,8 +10,9 @@ It implements a real local OpenClaw → n8n router. - accepts `POST /webhook/openclaw-action` - normalizes incoming JSON into an action contract -- supports two live actions: +- supports three live actions: - `append_log` + - `get_logs` - `notify` - returns normalized JSON responses - returns `400` for unknown actions @@ -32,6 +33,24 @@ Example stored record: {"ts":"2026-03-12T07:00:00Z","source":"openclaw-action","request_id":"abc","text":"backup complete"} ``` +### `get_logs` + +- reads from workflow static data key: + - `actionLog` +- returns newest-first +- default `limit` is `20` +- clamps `limit` to `1..50` + +### `notify` + +- sends a Telegram message using credential: + - `Telegram Bot (OpenClaw)` +- sends a Discord message using credential: + - `Discord Bot Auth` +- current targets mirror the already-working reminder workflow + +## Why workflow static data for logs + Why this first: - built-in, no extra credentials - persists without guessing writable filesystem paths @@ -43,14 +62,6 @@ When to use MinIO later: - large/batched exports - sharing logs outside n8n -### `notify` - -- sends a Telegram message using credential: - - `Telegram Bot (OpenClaw)` -- sends a Discord message using credential: - - `Discord Bot Auth` -- current targets mirror the already-working reminder workflow - ## Intentional security choice The exported workflow leaves the Webhook node auth unset in the JSON file. @@ -92,6 +103,7 @@ After import, set this manually in n8n: ```bash export N8N_WEBHOOK_SECRET='YOUR_SECRET_HERE' scripts/call-action.sh append_log --args '{"text":"backup complete"}' --pretty +scripts/call-action.sh get_logs --args '{"limit":5}' --pretty scripts/call-action.sh notify --args '{"title":"Workflow finished","message":"n8n router test"}' --pretty ``` @@ -118,6 +130,30 @@ scripts/call-action.sh notify --args '{"title":"Workflow finished","message":"n8 } ``` +### get_logs + +```json +{ + "ok": true, + "request_id": "", + "result": { + "action": "get_logs", + "status": "ok", + "count": 1, + "total_retained": 1, + "retained_entries": 200, + "entries": [ + { + "ts": "2026-03-12T08:42:37.615Z", + "source": "openclaw-action", + "request_id": "live-log-003", + "text": "n8n append_log static-data verification" + } + ] + } +} +``` + ### notify ```json diff --git a/skills/n8n-webhook/references/payloads.md b/skills/n8n-webhook/references/payloads.md index 2a32a98..50d91c8 100644 --- a/skills/n8n-webhook/references/payloads.md +++ b/skills/n8n-webhook/references/payloads.md @@ -74,6 +74,28 @@ Current sink: - key: `actionLog` - retained entries: `200` +### `get_logs` + +Request: + +```json +{ + "action": "get_logs", + "args": { + "limit": 10 + } +} +``` + +Purpose: +- return the most recent retained log records from workflow static data + +Behavior: +- default limit: `20` +- min limit: `1` +- max limit: `50` +- entries are returned newest-first + Success shape: ```json @@ -81,13 +103,12 @@ Success shape: "ok": true, "request_id": "optional-uuid", "result": { - "action": "append_log", - "status": "logged", - "sink": { - "type": "workflow-static-data", - "key": "actionLog", - "retained_entries": 200 - } + "action": "get_logs", + "status": "ok", + "count": 2, + "total_retained": 7, + "retained_entries": 200, + "entries": [] } } ``` @@ -140,5 +161,5 @@ Success shape: - Use lowercase kebab-case for webhook paths. - Use lowercase snake_case or kebab-case consistently for action names; prefer snake_case for JSON actions if using switch/router logic. -- Keep names explicit: `openclaw-ping`, `openclaw-action`, `append_log`, `notify`. +- Keep names explicit: `openclaw-ping`, `openclaw-action`, `append_log`, `get_logs`, `notify`. - Avoid generic names like `run`, `task`, or `webhook1`. diff --git a/skills/n8n-webhook/scripts/validate-workflow.py b/skills/n8n-webhook/scripts/validate-workflow.py index 9fbff65..5d34a2d 100755 --- a/skills/n8n-webhook/scripts/validate-workflow.py +++ b/skills/n8n-webhook/scripts/validate-workflow.py @@ -72,7 +72,7 @@ def main(): router = by_name['route-action'].get('parameters', {}) js_code = router.get('jsCode', '') - for snippet in ('append_log', 'notify', 'unknown_action', 'invalid_request', '$getWorkflowStaticData', 'actionLog', 'retained_entries', 'notify_text'): + for snippet in ('append_log', 'get_logs', 'notify', 'unknown_action', 'invalid_request', '$getWorkflowStaticData', 'actionLog', 'retained_entries', 'notify_text', 'entries.length', 'Math.min(50'): if snippet not in js_code: fail(f'route-action jsCode missing expected snippet: {snippet!r}') @@ -102,7 +102,7 @@ def main(): print('OK: workflow asset structure looks consistent') print(f'- workflow: {path}') print(f'- nodes: {len(nodes)}') - print('- routes: append_log -> workflow static data, notify -> Telegram + Discord, fallback -> JSON error') + print('- routes: append_log + get_logs via workflow static data, notify via Telegram + Discord, fallback -> JSON error') print('- samples: test-append-log.json, test-notify.json')