feat(n8n): add get-logs action
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user