feat(skill): finish n8n webhook skill
This commit is contained in:
@@ -9,4 +9,5 @@
|
|||||||
- `openclaw-ping` webhook path tested end-to-end
|
- `openclaw-ping` webhook path tested end-to-end
|
||||||
- Operating note: prefer narrow webhook-first integration rather than broad n8n admin/API access.
|
- Operating note: prefer narrow webhook-first integration rather than broad n8n admin/API access.
|
||||||
- Will clarified the primary host LAN IP to use/document is `192.168.153.113`.
|
- Will clarified the primary host LAN IP to use/document is `192.168.153.113`.
|
||||||
- Drafted local skill `skills/n8n-webhook` for authenticated webhook-first n8n integration, including `scripts/call-webhook.sh`, `scripts/call-action.sh`, payload notes, and a successful package/validation run to `/tmp/n8n-skill-dist/n8n-webhook.skill`.
|
- 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.
|
||||||
|
|||||||
@@ -20,7 +20,18 @@ Keep the integration narrow: let OpenClaw decide what to do, and let n8n execute
|
|||||||
4. Use header auth by default (`x-openclaw-secret`).
|
4. Use header auth by default (`x-openclaw-secret`).
|
||||||
5. Use `/webhook-test/` only while building/editing a workflow.
|
5. Use `/webhook-test/` only while building/editing a workflow.
|
||||||
6. Surface non-2xx responses clearly instead of pretending success.
|
6. Surface non-2xx responses clearly instead of pretending success.
|
||||||
7. If a new workflow is needed, define its request/response contract before wiring callers.
|
7. Keep secrets in n8n credentials or local env vars, never inside shareable workflow JSON.
|
||||||
|
8. If a new workflow is needed, define its request/response contract before wiring callers.
|
||||||
|
|
||||||
|
## What ships with this skill
|
||||||
|
|
||||||
|
- direct webhook caller: `scripts/call-webhook.sh`
|
||||||
|
- action-bus caller: `scripts/call-action.sh`
|
||||||
|
- workflow validator: `scripts/validate-workflow.py`
|
||||||
|
- importable router workflow: `assets/openclaw-action.workflow.json`
|
||||||
|
- sample payloads:
|
||||||
|
- `assets/test-append-log.json`
|
||||||
|
- `assets/test-notify.json`
|
||||||
|
|
||||||
## Quick usage
|
## Quick usage
|
||||||
|
|
||||||
@@ -39,7 +50,7 @@ scripts/call-webhook.sh openclaw-ping --data '{"message":"hello from OpenClaw"}'
|
|||||||
Call the preferred action-bus route:
|
Call the preferred action-bus route:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scripts/call-action.sh append_log --args '{"text":"backup complete"}'
|
scripts/call-action.sh append_log --args '{"text":"backup complete"}' --request-id auto
|
||||||
```
|
```
|
||||||
|
|
||||||
Call a test webhook while editing a flow:
|
Call a test webhook while editing a flow:
|
||||||
@@ -48,6 +59,12 @@ Call a test webhook while editing a flow:
|
|||||||
scripts/call-action.sh notify --args '{"message":"hello from OpenClaw"}' --test --pretty
|
scripts/call-action.sh notify --args '{"message":"hello from OpenClaw"}' --test --pretty
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Validate the shipped workflow asset:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/validate-workflow.py assets/openclaw-action.workflow.json
|
||||||
|
```
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
### Call an existing safe webhook directly
|
### Call an existing safe webhook directly
|
||||||
@@ -76,12 +93,28 @@ Payload shape:
|
|||||||
|
|
||||||
This keeps the external surface small while letting n8n route internally.
|
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:
|
||||||
|
|
||||||
|
- `append_log`
|
||||||
|
- `notify`
|
||||||
|
- 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`
|
||||||
|
|
||||||
|
See `references/openclaw-action.md` for import and test steps.
|
||||||
|
|
||||||
### Add a new webhook-backed capability
|
### Add a new webhook-backed capability
|
||||||
|
|
||||||
1. Write down the webhook path, required auth, request JSON, and response JSON.
|
1. Write down the webhook path, required auth, request JSON, and response JSON.
|
||||||
2. If the path should become part of the shared action bus, document the `action` name and `args` shape in `references/payloads.md`.
|
2. If the path should become part of the shared action bus, document the `action` name and `args` shape in `references/payloads.md`.
|
||||||
3. Keep the first version small and explicit.
|
3. If the shipped workflow should support it, update `assets/openclaw-action.workflow.json` and rerun `scripts/validate-workflow.py`.
|
||||||
4. Only add the new endpoint to regular use after a successful `/webhook-test/` run.
|
4. Keep the first version small and explicit.
|
||||||
|
5. Only add the new endpoint to regular use after a successful `/webhook-test/` run.
|
||||||
|
|
||||||
## Environment variables
|
## Environment variables
|
||||||
|
|
||||||
@@ -94,4 +127,7 @@ This keeps the external surface small while letting n8n route internally.
|
|||||||
|
|
||||||
- `scripts/call-webhook.sh` — authenticated POST helper for direct local n8n webhooks
|
- `scripts/call-webhook.sh` — authenticated POST helper for direct local n8n webhooks
|
||||||
- `scripts/call-action.sh` — wrapper for action-bus style calls against `openclaw-action`
|
- `scripts/call-action.sh` — wrapper for action-bus style calls against `openclaw-action`
|
||||||
|
- `scripts/validate-workflow.py` — local structural validator for the shipped workflow asset
|
||||||
|
- `assets/openclaw-action.workflow.json` — importable starter workflow for the action bus
|
||||||
|
- `references/openclaw-action.md` — import, auth-binding, and testing guide
|
||||||
- `references/payloads.md` — request/response contracts and naming conventions
|
- `references/payloads.md` — request/response contracts and naming conventions
|
||||||
|
|||||||
369
skills/n8n-webhook/assets/openclaw-action.workflow.json
Normal file
369
skills/n8n-webhook/assets/openclaw-action.workflow.json
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
{
|
||||||
|
"name": "openclaw-action",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "webhook-openclaw-action",
|
||||||
|
"name": "Webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2.1,
|
||||||
|
"position": [
|
||||||
|
-820,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "openclaw-action",
|
||||||
|
"authentication": "none",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"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,
|
||||||
|
"position": [
|
||||||
|
-300,
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"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,
|
||||||
|
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}}",
|
||||||
|
"options": {
|
||||||
|
"responseCode": "={{$json.status_code}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "normalize-request",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"normalize-request": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "route-action",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"route-action": {
|
||||||
|
"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",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pinData": {},
|
||||||
|
"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. Secrets are intentionally not embedded in the workflow export."
|
||||||
|
},
|
||||||
|
"active": false,
|
||||||
|
"versionId": "openclaw-action-v1"
|
||||||
|
}
|
||||||
7
skills/n8n-webhook/assets/test-append-log.json
Normal file
7
skills/n8n-webhook/assets/test-append-log.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"action": "append_log",
|
||||||
|
"args": {
|
||||||
|
"text": "backup complete"
|
||||||
|
},
|
||||||
|
"request_id": "test-append-log-001"
|
||||||
|
}
|
||||||
8
skills/n8n-webhook/assets/test-notify.json
Normal file
8
skills/n8n-webhook/assets/test-notify.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"action": "notify",
|
||||||
|
"args": {
|
||||||
|
"title": "Workflow finished",
|
||||||
|
"message": "n8n router test"
|
||||||
|
},
|
||||||
|
"request_id": "test-notify-001"
|
||||||
|
}
|
||||||
148
skills/n8n-webhook/references/openclaw-action.md
Normal file
148
skills/n8n-webhook/references/openclaw-action.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# openclaw-action workflow
|
||||||
|
|
||||||
|
This skill ships an importable draft workflow at:
|
||||||
|
|
||||||
|
- `assets/openclaw-action.workflow.json`
|
||||||
|
|
||||||
|
It implements the first safe router contract for local OpenClaw → n8n calls.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
- accepts `POST /webhook/openclaw-action`
|
||||||
|
- normalizes incoming JSON into:
|
||||||
|
- `action`
|
||||||
|
- `args`
|
||||||
|
- `request_id`
|
||||||
|
- routes two known actions:
|
||||||
|
- `append_log`
|
||||||
|
- `notify`
|
||||||
|
- returns normalized JSON responses
|
||||||
|
- returns `400` for unknown actions
|
||||||
|
- returns `400` when required branch args are missing
|
||||||
|
|
||||||
|
## Intentional security choice
|
||||||
|
|
||||||
|
The exported workflow leaves the Webhook node auth unset in the JSON file.
|
||||||
|
|
||||||
|
Why:
|
||||||
|
- n8n credentials are instance-local
|
||||||
|
- secrets should not be embedded in a shareable skill asset
|
||||||
|
|
||||||
|
After import, set this manually in n8n:
|
||||||
|
|
||||||
|
- Webhook node → **Authentication** → `Header Auth`
|
||||||
|
- bind a credential with:
|
||||||
|
- header name: `x-openclaw-secret`
|
||||||
|
- header value: your generated shared secret
|
||||||
|
|
||||||
|
## Import steps
|
||||||
|
|
||||||
|
1. In n8n, create or open a workflow.
|
||||||
|
2. Import `assets/openclaw-action.workflow.json`.
|
||||||
|
3. Open the **Webhook** node.
|
||||||
|
4. Set **Authentication** to `Header Auth`.
|
||||||
|
5. Bind your local credential.
|
||||||
|
6. Save.
|
||||||
|
7. Use **Listen for test event** and call the test URL first.
|
||||||
|
8. Once successful, activate the workflow for production URL use.
|
||||||
|
|
||||||
|
## 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`
|
||||||
|
|
||||||
|
## Test payloads included
|
||||||
|
|
||||||
|
- `assets/test-append-log.json`
|
||||||
|
- `assets/test-notify.json`
|
||||||
|
|
||||||
|
## 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected success examples
|
||||||
|
|
||||||
|
### append_log
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"request_id": "test-append-log-001",
|
||||||
|
"result": {
|
||||||
|
"action": "append_log",
|
||||||
|
"status": "accepted",
|
||||||
|
"preview": {
|
||||||
|
"text": "backup complete"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### notify
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"request_id": "test-notify-001",
|
||||||
|
"result": {
|
||||||
|
"action": "notify",
|
||||||
|
"status": "accepted",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run the local validator before import/package changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/validate-workflow.py assets/openclaw-action.workflow.json
|
||||||
|
```
|
||||||
@@ -33,6 +33,9 @@ Purpose:
|
|||||||
- keep the external n8n surface small
|
- keep the external n8n surface small
|
||||||
- route several agent-safe operations behind one authenticated webhook
|
- route several agent-safe operations behind one authenticated webhook
|
||||||
|
|
||||||
|
Shipped workflow asset:
|
||||||
|
- `assets/openclaw-action.workflow.json`
|
||||||
|
|
||||||
Recommended request shape:
|
Recommended request shape:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -87,6 +90,9 @@ Request:
|
|||||||
Purpose:
|
Purpose:
|
||||||
- append a short line to a known log or tracking sink
|
- append a short line to a known log or tracking sink
|
||||||
|
|
||||||
|
Sample payload file:
|
||||||
|
- `assets/test-append-log.json`
|
||||||
|
|
||||||
### `notify`
|
### `notify`
|
||||||
|
|
||||||
Request:
|
Request:
|
||||||
@@ -104,6 +110,9 @@ Request:
|
|||||||
Purpose:
|
Purpose:
|
||||||
- send a small notification through a known downstream channel
|
- send a small notification through a known downstream channel
|
||||||
|
|
||||||
|
Sample payload file:
|
||||||
|
- `assets/test-notify.json`
|
||||||
|
|
||||||
## Naming guidance
|
## Naming guidance
|
||||||
|
|
||||||
- Use lowercase kebab-case for webhook paths.
|
- Use lowercase kebab-case for webhook paths.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ set -euo pipefail
|
|||||||
usage() {
|
usage() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
Usage:
|
Usage:
|
||||||
scripts/call-action.sh <action> [--args '{"k":"v"}'] [--args-file args.json] [--request-id <id>] [--path openclaw-action] [--test] [--pretty] [--dry-run]
|
scripts/call-action.sh <action> [--args '{"k":"v"}'] [--args-file args.json] [--request-id <id|auto>] [--path openclaw-action] [--test] [--pretty] [--dry-run]
|
||||||
|
|
||||||
Environment:
|
Environment:
|
||||||
N8N_ACTION_PATH Default router webhook path (default: openclaw-action)
|
N8N_ACTION_PATH Default router webhook path (default: openclaw-action)
|
||||||
@@ -13,7 +13,7 @@ Environment:
|
|||||||
N8N_SECRET_HEADER Header name (default: x-openclaw-secret)
|
N8N_SECRET_HEADER Header name (default: x-openclaw-secret)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
scripts/call-action.sh append_log --args '{"text":"backup complete"}'
|
scripts/call-action.sh append_log --args '{"text":"backup complete"}' --request-id auto
|
||||||
scripts/call-action.sh notify --args-file notify.json --test --pretty
|
scripts/call-action.sh notify --args-file notify.json --test --pretty
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@@ -98,6 +98,14 @@ if [[ -n "$ARGS_FILE" ]]; then
|
|||||||
ARGS="$(cat "$ARGS_FILE")"
|
ARGS="$(cat "$ARGS_FILE")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "$REQUEST_ID" == "auto" ]]; then
|
||||||
|
REQUEST_ID="$(python3 - <<'PY'
|
||||||
|
import uuid
|
||||||
|
print(uuid.uuid4())
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
PAYLOAD="$({
|
PAYLOAD="$({
|
||||||
python3 - <<'PY' "$ACTION" "$ARGS" "$REQUEST_ID"
|
python3 - <<'PY' "$ACTION" "$ARGS" "$REQUEST_ID"
|
||||||
import json, sys
|
import json, sys
|
||||||
|
|||||||
128
skills/n8n-webhook/scripts/validate-workflow.py
Executable file
128
skills/n8n-webhook/scripts/validate-workflow.py
Executable file
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fail(msg: str):
|
||||||
|
print(f'ERROR: {msg}', file=sys.stderr)
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def load_json(path: Path):
|
||||||
|
try:
|
||||||
|
return json.loads(path.read_text())
|
||||||
|
except Exception as e:
|
||||||
|
fail(f'failed to parse {path}: {e}')
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
fail('workflow is missing a nodes array')
|
||||||
|
if not isinstance(connections, dict):
|
||||||
|
fail('workflow is missing a connections object')
|
||||||
|
|
||||||
|
by_name = {}
|
||||||
|
for node in nodes:
|
||||||
|
if not isinstance(node, dict):
|
||||||
|
fail('every node entry must be an object')
|
||||||
|
name = node.get('name')
|
||||||
|
if not name:
|
||||||
|
fail('every node must have a name')
|
||||||
|
by_name[name] = node
|
||||||
|
|
||||||
|
missing = sorted(REQUIRED_NODE_NAMES - set(by_name))
|
||||||
|
if missing:
|
||||||
|
fail(f'missing required nodes: {", ".join(missing)}')
|
||||||
|
|
||||||
|
for name, node_type in EXPECTED_DIRECT_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':
|
||||||
|
fail('Webhook.path must be openclaw-action')
|
||||||
|
if webhook_params.get('httpMethod') != 'POST':
|
||||||
|
fail('Webhook.httpMethod must be POST')
|
||||||
|
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}')
|
||||||
|
|
||||||
|
route_outputs = connections.get('route-action', {}).get('main', [])
|
||||||
|
if len(route_outputs) < 3:
|
||||||
|
fail('route-action must expose append_log, notify, and fallback outputs')
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
sample_paths = [
|
||||||
|
path.parent / 'test-append-log.json',
|
||||||
|
path.parent / 'test-notify.json',
|
||||||
|
]
|
||||||
|
for sample in sample_paths:
|
||||||
|
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:
|
||||||
|
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(f'- rules: {len(rule_values)} + fallback')
|
||||||
|
print('- samples: test-append-log.json, test-notify.json')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user