feat(n8n): wire notify and persist append logs
This commit is contained in:
@@ -95,16 +95,17 @@ This keeps the external surface small while letting n8n route internally.
|
||||
|
||||
### Import the shipped router workflow
|
||||
|
||||
Use the included workflow asset when you want a ready-made starter router for:
|
||||
Use the included workflow asset when you want a ready-made local router for:
|
||||
|
||||
- `append_log`
|
||||
- `notify`
|
||||
- `append_log` → append small records into workflow static data (`actionLog`, latest 200)
|
||||
- `notify` → send through the current Telegram + Discord notification paths
|
||||
- normalized JSON success/failure responses
|
||||
- unknown-action handling
|
||||
|
||||
Important:
|
||||
- the workflow export intentionally leaves Webhook authentication unset
|
||||
- after import, manually set **Authentication = Header Auth** on the Webhook node and bind a local credential using `x-openclaw-secret`
|
||||
- the shipped asset already includes the live side-effect shape for local JSONL logging plus Telegram/Discord fan-out
|
||||
|
||||
See `references/openclaw-action.md` for import and test steps.
|
||||
|
||||
@@ -115,6 +116,7 @@ See `references/openclaw-action.md` for import and test steps.
|
||||
3. If the shipped workflow should support it, update `assets/openclaw-action.workflow.json` and rerun `scripts/validate-workflow.py`.
|
||||
4. Keep the first version small and explicit.
|
||||
5. Only add the new endpoint to regular use after a successful `/webhook-test/` run.
|
||||
6. For append-style event logging, prefer workflow static data for small recent breadcrumbs; use MinIO later for rotation, batching, archival, or sharing rather than tiny object-per-line writes.
|
||||
|
||||
## Environment variables
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2.1,
|
||||
"position": [
|
||||
-360,
|
||||
0
|
||||
-700,
|
||||
40
|
||||
],
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
@@ -24,13 +24,103 @@
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
-40,
|
||||
0
|
||||
-420,
|
||||
40
|
||||
],
|
||||
"parameters": {
|
||||
"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};"
|
||||
"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};"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -39,14 +129,14 @@
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.5,
|
||||
"position": [
|
||||
260,
|
||||
0
|
||||
760,
|
||||
40
|
||||
],
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{$json.response_body}}",
|
||||
"responseBody": "={{$node[\"route-action\"].json[\"response_body\"]}}",
|
||||
"options": {
|
||||
"responseCode": "={{$json.status_code}}"
|
||||
"responseCode": "={{$node[\"route-action\"].json[\"status_code\"]}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +154,46 @@
|
||||
]
|
||||
},
|
||||
"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": [
|
||||
[
|
||||
{
|
||||
@@ -81,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. Secrets are intentionally not embedded in the workflow export."
|
||||
"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."
|
||||
},
|
||||
"active": false,
|
||||
"versionId": "openclaw-action-v2"
|
||||
"versionId": "openclaw-action-v5"
|
||||
}
|
||||
|
||||
@@ -1,24 +1,55 @@
|
||||
# openclaw-action workflow
|
||||
|
||||
This skill ships an importable draft workflow at:
|
||||
This skill ships an importable workflow at:
|
||||
|
||||
- `assets/openclaw-action.workflow.json`
|
||||
|
||||
It implements the first safe router contract for local OpenClaw → n8n calls.
|
||||
It implements a real local OpenClaw → n8n router.
|
||||
|
||||
## What it does
|
||||
|
||||
- accepts `POST /webhook/openclaw-action`
|
||||
- normalizes incoming JSON into:
|
||||
- `action`
|
||||
- `args`
|
||||
- `request_id`
|
||||
- routes two known actions:
|
||||
- normalizes incoming JSON into an action contract
|
||||
- supports two live actions:
|
||||
- `append_log`
|
||||
- `notify`
|
||||
- returns normalized JSON responses
|
||||
- returns `400` for unknown actions
|
||||
- returns `400` when required branch args are missing
|
||||
- returns `400` when required args are missing
|
||||
|
||||
## Current side effects
|
||||
|
||||
### `append_log`
|
||||
|
||||
- appends records into workflow static data under key:
|
||||
- `actionLog`
|
||||
- keeps the most recent `200` entries
|
||||
- persists in n8n's database when the workflow execution succeeds
|
||||
|
||||
Example stored record:
|
||||
|
||||
```json
|
||||
{"ts":"2026-03-12T07:00:00Z","source":"openclaw-action","request_id":"abc","text":"backup complete"}
|
||||
```
|
||||
|
||||
Why this first:
|
||||
- built-in, no extra credentials
|
||||
- persists without guessing writable filesystem paths
|
||||
- better fit than MinIO for small, recent operational breadcrumbs
|
||||
|
||||
When to use MinIO later:
|
||||
- long retention
|
||||
- rotated archives
|
||||
- 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
|
||||
|
||||
@@ -48,8 +79,6 @@ After import, set this manually in n8n:
|
||||
|
||||
## Expected URLs
|
||||
|
||||
Assuming the current local service address:
|
||||
|
||||
- test: `http://192.168.153.113:18808/webhook-test/openclaw-action`
|
||||
- prod: `http://192.168.153.113:18808/webhook/openclaw-action`
|
||||
|
||||
@@ -60,20 +89,10 @@ Assuming the current local service address:
|
||||
|
||||
## Example tests
|
||||
|
||||
Direct curl:
|
||||
|
||||
```bash
|
||||
curl -i -X POST 'http://192.168.153.113:18808/webhook-test/openclaw-action' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'x-openclaw-secret: YOUR_SECRET_HERE' \
|
||||
--data @assets/test-append-log.json
|
||||
```
|
||||
|
||||
Via skill helper:
|
||||
|
||||
```bash
|
||||
export N8N_WEBHOOK_SECRET='YOUR_SECRET_HERE'
|
||||
scripts/call-action.sh append_log --args '{"text":"backup complete"}' --test --pretty
|
||||
scripts/call-action.sh append_log --args '{"text":"backup complete"}' --pretty
|
||||
scripts/call-action.sh notify --args '{"title":"Workflow finished","message":"n8n router test"}' --pretty
|
||||
```
|
||||
|
||||
## Expected success examples
|
||||
@@ -86,9 +105,14 @@ scripts/call-action.sh append_log --args '{"text":"backup complete"}' --test --p
|
||||
"request_id": "test-append-log-001",
|
||||
"result": {
|
||||
"action": "append_log",
|
||||
"status": "accepted",
|
||||
"status": "logged",
|
||||
"preview": {
|
||||
"text": "backup complete"
|
||||
},
|
||||
"sink": {
|
||||
"type": "workflow-static-data",
|
||||
"key": "actionLog",
|
||||
"retained_entries": 200
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,39 +126,12 @@ scripts/call-action.sh append_log --args '{"text":"backup complete"}' --test --p
|
||||
"request_id": "test-notify-001",
|
||||
"result": {
|
||||
"action": "notify",
|
||||
"status": "accepted",
|
||||
"status": "sent",
|
||||
"preview": {
|
||||
"title": "Workflow finished",
|
||||
"message": "n8n router test"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Expected failure examples
|
||||
|
||||
### unknown action
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"request_id": "",
|
||||
"error": {
|
||||
"code": "unknown_action",
|
||||
"message": "action is not supported"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### missing required args
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"request_id": "",
|
||||
"error": {
|
||||
"code": "invalid_request",
|
||||
"message": "required args are missing"
|
||||
},
|
||||
"targets": ["telegram", "discord"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -48,31 +48,7 @@ Recommended request shape:
|
||||
}
|
||||
```
|
||||
|
||||
Recommended success response:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"request_id": "optional-uuid",
|
||||
"result": {
|
||||
"status": "accepted"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Recommended failure response:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": "unknown_action",
|
||||
"message": "action is not supported"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Suggested initial actions
|
||||
## Live actions
|
||||
|
||||
### `append_log`
|
||||
|
||||
@@ -82,16 +58,39 @@ Request:
|
||||
{
|
||||
"action": "append_log",
|
||||
"args": {
|
||||
"text": "backup complete"
|
||||
"text": "backup complete",
|
||||
"meta": {
|
||||
"source": "backup-job"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Purpose:
|
||||
- append a short line to a known log or tracking sink
|
||||
- append one small operational breadcrumb into n8n workflow static data
|
||||
|
||||
Sample payload file:
|
||||
- `assets/test-append-log.json`
|
||||
Current sink:
|
||||
- type: `workflow-static-data`
|
||||
- key: `actionLog`
|
||||
- retained entries: `200`
|
||||
|
||||
Success shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"request_id": "optional-uuid",
|
||||
"result": {
|
||||
"action": "append_log",
|
||||
"status": "logged",
|
||||
"sink": {
|
||||
"type": "workflow-static-data",
|
||||
"key": "actionLog",
|
||||
"retained_entries": 200
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `notify`
|
||||
|
||||
@@ -108,10 +107,34 @@ Request:
|
||||
```
|
||||
|
||||
Purpose:
|
||||
- send a small notification through a known downstream channel
|
||||
- send the message through the currently configured Telegram + Discord notification targets
|
||||
|
||||
Sample payload file:
|
||||
- `assets/test-notify.json`
|
||||
Success shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"request_id": "optional-uuid",
|
||||
"result": {
|
||||
"action": "notify",
|
||||
"status": "sent",
|
||||
"targets": ["telegram", "discord"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Failure shape
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"request_id": "optional-uuid",
|
||||
"error": {
|
||||
"code": "unknown_action",
|
||||
"message": "action is not supported"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Naming guidance
|
||||
|
||||
|
||||
@@ -6,12 +6,18 @@ from pathlib import Path
|
||||
REQUIRED_NODE_NAMES = {
|
||||
'Webhook',
|
||||
'route-action',
|
||||
'route-dispatch',
|
||||
'Send Telegram Notification',
|
||||
'Send Discord Notification',
|
||||
'Respond to Webhook',
|
||||
}
|
||||
|
||||
EXPECTED_DIRECT_TYPES = {
|
||||
EXPECTED_TYPES = {
|
||||
'Webhook': 'n8n-nodes-base.webhook',
|
||||
'route-action': 'n8n-nodes-base.code',
|
||||
'route-dispatch': 'n8n-nodes-base.switch',
|
||||
'Send Telegram Notification': 'n8n-nodes-base.telegram',
|
||||
'Send Discord Notification': 'n8n-nodes-base.httpRequest',
|
||||
'Respond to Webhook': 'n8n-nodes-base.respondToWebhook',
|
||||
}
|
||||
|
||||
@@ -31,10 +37,6 @@ def load_json(path: Path):
|
||||
def main():
|
||||
path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path('assets/openclaw-action.workflow.json')
|
||||
data = load_json(path)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
fail('workflow file must decode to a JSON object')
|
||||
|
||||
nodes = data.get('nodes')
|
||||
connections = data.get('connections')
|
||||
if not isinstance(nodes, list):
|
||||
@@ -55,55 +57,52 @@ def main():
|
||||
if missing:
|
||||
fail(f'missing required nodes: {", ".join(missing)}')
|
||||
|
||||
for name, node_type in EXPECTED_DIRECT_TYPES.items():
|
||||
for name, node_type in EXPECTED_TYPES.items():
|
||||
actual = by_name[name].get('type')
|
||||
if actual != node_type:
|
||||
fail(f'node {name!r} should have type {node_type!r}, got {actual!r}')
|
||||
|
||||
webhook = by_name['Webhook']
|
||||
webhook_params = webhook.get('parameters', {})
|
||||
if webhook_params.get('path') != 'openclaw-action':
|
||||
webhook = by_name['Webhook'].get('parameters', {})
|
||||
if webhook.get('path') != 'openclaw-action':
|
||||
fail('Webhook.path must be openclaw-action')
|
||||
if webhook_params.get('httpMethod') != 'POST':
|
||||
if webhook.get('httpMethod') != 'POST':
|
||||
fail('Webhook.httpMethod must be POST')
|
||||
if webhook_params.get('responseMode') != 'responseNode':
|
||||
if webhook.get('responseMode') != 'responseNode':
|
||||
fail('Webhook.responseMode must be responseNode')
|
||||
|
||||
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"):
|
||||
for snippet in ('append_log', 'notify', 'unknown_action', 'invalid_request', '$getWorkflowStaticData', 'actionLog', 'retained_entries', 'notify_text'):
|
||||
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) < 1:
|
||||
fail('route-action must connect to Respond to Webhook')
|
||||
switch = by_name['route-dispatch'].get('parameters', {})
|
||||
values = switch.get('rules', {}).get('values', [])
|
||||
names = {v.get('outputKey') for v in values if isinstance(v, dict)}
|
||||
if 'notify' not in names:
|
||||
fail('route-dispatch must route notify')
|
||||
|
||||
telegram = by_name['Send Telegram Notification']
|
||||
if telegram.get('credentials', {}).get('telegramApi', {}).get('name') != 'Telegram Bot (OpenClaw)':
|
||||
fail('Send Telegram Notification must use Telegram Bot (OpenClaw) credential')
|
||||
|
||||
discord = by_name['Send Discord Notification']
|
||||
if discord.get('credentials', {}).get('httpHeaderAuth', {}).get('name') != 'Discord Bot Auth':
|
||||
fail('Send Discord Notification must use Discord Bot Auth credential')
|
||||
|
||||
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',
|
||||
path.parent / 'test-notify.json',
|
||||
]
|
||||
for sample in sample_paths:
|
||||
for sample in (path.parent / 'test-append-log.json', path.parent / 'test-notify.json'):
|
||||
sample_data = load_json(sample)
|
||||
if not isinstance(sample_data, dict):
|
||||
fail(f'sample payload must be an object: {sample}')
|
||||
if 'action' not in sample_data or 'args' not in sample_data:
|
||||
if not isinstance(sample_data, dict) or 'action' not in sample_data or 'args' not in sample_data:
|
||||
fail(f'sample payload missing action/args: {sample}')
|
||||
|
||||
print('OK: workflow asset structure looks consistent')
|
||||
print(f'- workflow: {path}')
|
||||
print(f'- nodes: {len(nodes)}')
|
||||
print('- router: code node with append_log + notify + fallback')
|
||||
print('- routes: append_log -> workflow static data, notify -> Telegram + Discord, fallback -> JSON error')
|
||||
print('- samples: test-append-log.json, test-notify.json')
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user