Skill: droidhackzor/session-openclaw-bridge (version 0.1.0)

--- name: session-openclaw-bridge description: Configure, validate, and troubleshoot a Session Messenger ↔ n8n ↔ OpenClaw bridge for two-way text messaging and image exchange when the hosted relay supports attachments. Use when setting up or repairing the n8n/OpenClaw side of a Session relay, importing or patching the n8n workflow, validating relay endpoints like /health, /poll, and /send, checking whether the deployed relay matches the expected API, confirming attachment capability for photos/files, or preparing the integration for publication on ClawHub. --- # Session ↔ OpenClaw bridge Use this skill to manage the OpenClaw and n8n side of a Session Messenger relay. ## Scope Handle: - n8n workflow generation/import/patching - relay health validation - endpoint verification for `/health`, `/poll`, `/send`, and attachment download when present - mismatch detection between deployed relay and expected API - OpenClaw endpoint wiring guidance - packaging the skill for reuse or publication Do not assume the Session relay should implement OpenClaw chat generation internally. Keep the model: - relay handles Session connectivity and exposes HTTP endpoints - n8n orchestrates polling, filtering, and reply delivery - OpenClaw generates replies ## Primary files Read these when needed: - `references/openclaw-session-relay-workflow.json` — n8n workflow template - `references/release.md` — shared publication version for the skill and relay project - `scripts/validate_relay.py` — probe relay endpoints and summarize readiness ## Expected relay API Expected minimum HTTP API: - `GET /health` - `GET /poll?since=<seq>&limit=<n>` - `POST /send` Optional attachment endpoint for image/file workflows: - `GET /attachment/:messageId/:attachmentId` Expected `/health` fields for a correct modern deployment: - `ok` - `sessionId` - `inboundEnabled` - `queued` - `lastSeq` If `/poll` returns 404 or `/health` omits the inbound queue fields, treat the deployed relay as an older/wrong build. ## n8n workflow model Preferred flow: 1. Cron trigger 2. Poll Session relay `/poll` 3. Extract next cursor 4. Split messages 5. Filter allowed sender 6. POST message text to OpenClaw endpoint 7. POST reply to relay `/send` The relay is text-first but may support image/file exchange when its `/health` capabilities and implementation confirm attachment receive/send support. Do not promise calls or live media. ## OpenClaw endpoint note Do not invent the OpenClaw HTTP endpoint. Confirm the actual endpoint and payload shape before activation. ## Packaging Current shared release version: `0.1.0` Package with: ```bash scripts/package_skill.py /home/openclaw/.openclaw/workspace/skills/session-openclaw-bridge ``` If preparing for ClawHub, ensure: - the skill release version matches the hosted relay project version - the packaged skill contains only: - `SKILL.md` - minimal scripts - references actually used by the skill

path: references/openclaw-session-relay-workflow.json
content: {
  "name": "OpenClaw ↔ Session Relay (Rebuilt v2)",
  "nodes": [
    {
      "parameters": {
        "triggerTimes": {
          "item": [
            {
              "mode": "everyX",
              "value": 10,
              "unit": "seconds"
            }
          ]
        }
      },
      "id": "cron-poll",
      "name": "Cron (poll)",
      "type": "n8n-nodes-base.cron",
      "typeVersion": 1,
      "position": [220, 280]
    },
    {
      "parameters": {
        "url": "http://REPLACE_SESSION_BOT_HOST:3030/poll?since={{$env.SESSION_POLL_SINCE || 0}}&limit=50",
        "method": "GET",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-API-Key",
              "value": "REPLACE_SESSION_BOT_API_KEY"
            }
          ]
        },
        "responseFormat": "json"
      },
      "id": "poll-session",
      "name": "Poll Session Bot (/poll)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [460, 280]
    },
    {
      "parameters": {
        "keepOnlySet": true,
        "values": {
          "string": [
            {
              "name": "next",
              "value": "={{$json.next || 0}}"
            }
          ]
        }
      },
      "id": "extract-next",
      "name": "Extract next cursor",
      "type": "n8n-nodes-base.set",
      "typeVersion": 2,
      "position": [700, 130]
    },
    {
      "parameters": {
        "values": {
          "string": [
            {
              "name": "SESSION_POLL_SINCE",
              "value": "={{$json.next}}"
            }
          ]
        }
      },
      "id": "cursor-note",
      "name": "Cursor note (replace me)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 2,
      "position": [940, 130]
    },
    {
      "parameters": {
        "fieldToSplitOut": "messages"
      },
      "id": "split-msgs",
      "name": "Split messages",
      "type": "n8n-nodes-base.itemLists",
      "typeVersion": 1,
      "position": [700, 280]
    },
    {
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{$json.from}}",
              "operation": "equals",
              "value2": "={{$env.ALLOW_FROM || 'REPLACE_ALLOWED_SESSION_ID'}}"
            }
          ]
        }
      },
      "id": "if-allow",
      "name": "IF sender allowed",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [940, 280]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "REPLACE_OPENCALW_ENDPOINT",
        "responseFormat": "json",
        "jsonParameters": true,
        "bodyParametersJson": "={\"input\": $json.text, \"from\": $json.from}"
      },
      "id": "openclaw-reply",
      "name": "OpenClaw (generate reply)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [1180, 170]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://REPLACE_SESSION_BOT_HOST:3030/send",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-API-Key",
              "value": "REPLACE_SESSION_BOT_API_KEY"
            }
          ]
        },
        "responseFormat": "json",
        "jsonParameters": true,
        "bodyParametersJson": "={\"to\": $json.from, \"text\": ($node[\"OpenClaw (generate reply)\"].json.output || $node[\"OpenClaw (generate reply)\"].json.text || JSON.stringify($node[\"OpenClaw (generate reply)\"].json))}"
      },
      "id": "reply-send",
      "name": "Reply via Session Bot (/send)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [1420, 170]
    }
  ],
  "connections": {
    "Cron (poll)": {
      "main": [
        [
          {
            "node": "Poll Session Bot (/poll)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Poll Session Bot (/poll)": {
      "main": [
        [
          {
            "node": "Extract next cursor",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Split messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract next cursor": {
      "main": [
        [
          {
            "node": "Cursor note (replace me)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split messages": {
      "main": [
        [
          {
            "node": "IF sender allowed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF sender allowed": {
      "main": [
        [
          {
            "node": "OpenClaw (generate reply)",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "OpenClaw (generate reply)": {
      "main": [
        [
          {
            "node": "Reply via Session Bot (/send)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {}
}


path: references/release.md
content: # Release

- Version: 0.1.0
- Matching relay project version: 0.1.0
- Intended GitLab project: [REDACTED_SECRET]

Use this as the shared release number for the ClawHub skill and the hosted relay project.


path: scripts/validate_relay.py
content: #!/usr/bin/env python3
import json
import sys
import urllib.request
import urllib.error


def call(url, method="GET", headers=None, body=None, timeout=8):
    req = urllib.request.Request(url, method=method, headers=headers or {}, data=body)
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            raw = r.read().decode("utf-8", "ignore")
            try:
                parsed = json.loads(raw)
            except Exception:
                parsed = raw
            return {"status": r.status, "body": parsed}
    except urllib.error.HTTPError as e:
        raw = e.read().decode("utf-8", "ignore")
        try:
            parsed = json.loads(raw)
        except Exception:
            parsed = raw
        return {"status": e.code, "body": parsed}
    except Exception as e:
        return {"error": f"{type(e).__name__}: {e}"}


def main():
    if len(sys.argv) < 2:
        print("usage: validate_relay.py <base-url> [api-key]", file=sys.stderr)
        sys.exit(2)

    base = sys.argv[1].rstrip("/")
    [REDACTED_SECRET] if len(sys.argv) > 2 else None

    out = {}
    out["health"] = call(f"{base}/health")

    headers = {"Accept": "application/json"}
    if [REDACTED_SECRET]"X-API-Key"] = api_key

    out["poll"] = call(f"{base}/poll?since=0&limit=1", headers=headers)
    out["attachment_probe"] = call(f"{base}/attachment/test-message/test-attachment", headers=headers)

    send_headers = {"Content-Type": "application/json", "Accept": "application/json"}
    if [REDACTED_SECRET]"X-API-Key"] = api_key
    out["send_shape_check"] = call(
        f"{base}/send",
        method="POST",
        headers=send_headers,
        body=json.dumps({"to": "TEST", "text": "probe"}).encode(),
    )

    summary = {
        "reachable": isinstance(out["health"], dict) and out["health"].get("status") == 200,
        "poll_supported": isinstance(out["poll"], dict) and out["poll"].get("status") != 404,
        "attachment_endpoint_present": isinstance(out["attachment_probe"], dict) and out["attachment_probe"].get("status") != 404,
        "modern_health": False,
    }
    body = out.get("health", {}).get("body") if isinstance(out.get("health"), dict) else None
    if isinstance(body, dict):
        summary["modern_health"] = all(k in body for k in ["ok", "sessionId", "inboundEnabled", "queued", "lastSeq"])

    print(json.dumps({"summary": summary, "checks": out}, indent=2))


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)