{
  "id": "q3qmVNv1qZSnqYVr",
  "name": "Generate text with Claude and post it to Slack via a validated webhook",
  "tags": [],
  "nodes": [
    {
      "id": "pub001-sticky-main",
      "name": "Sticky Note: Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -896,
        -1120
      ],
      "parameters": {
        "width": 3000,
        "height": 520,
        "content": "## Claude (Anthropic) API caller \u2014 retry, validation, error handling & Slack delivery\n\nThis workflow exposes a webhook that safely wraps calls to the Anthropic Claude Messages API and posts the result to Slack. It validates the incoming request, fills in sensible defaults, retries automatically on transient failures, and always returns a predictable JSON response to the caller \u2014 either the generated text or a structured error \u2014 while also pushing the generated text to a Slack channel via an Incoming Webhook.\n\n### Try it\nSend a POST request with a JSON body such as `{\"userPrompt\": \"Write a two-line haiku about automation\"}`.\n\nOptional fields: systemPrompt, model (defaults to claude-sonnet-5), maxTokens (defaults to 4000), temperature (defaults to 1).\n\n### Before you activate\n1. Add an Anthropic API key as a credential in n8n (Credential type: Anthropic), and assign it in the \"Call Claude (Anthropic) API\" node.\n2. Create a Slack Incoming Webhook (no Slack app/OAuth needed) and paste its URL into the \"Set: Slack webhook URL\" node."
      },
      "typeVersion": 1
    },
    {
      "id": "pub001-sticky-step1",
      "name": "Sticky Note: Step 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -848,
        -528
      ],
      "parameters": {
        "width": 600,
        "height": 220,
        "content": "Step 1 \u2014 Receive & validate\n\nAccepts the incoming webhook POST request and checks that userPrompt is present. Fills in defaults for model, maxTokens and temperature. Invalid requests are rejected immediately with a 400 response instead of reaching the Claude API."
      },
      "typeVersion": 1
    },
    {
      "id": "pub001-sticky-step2",
      "name": "Sticky Note: Step 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -144,
        -528
      ],
      "parameters": {
        "width": 600,
        "height": 220,
        "content": "Step 2 \u2014 Call Claude\n\nBuilds the Anthropic Messages API request body and sends it with automatic retries (3 attempts, 2 second backoff) so short-lived network or rate-limit errors don't fail the whole request."
      },
      "typeVersion": 1
    },
    {
      "id": "pub001-sticky-step3",
      "name": "Sticky Note: Step 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        560,
        -528
      ],
      "parameters": {
        "width": 600,
        "height": 220,
        "content": "Step 3 \u2014 Respond\n\nOn success, returns the generated text plus token usage as JSON (200) right away. If the API call still fails after retries, returns a structured error response (502) instead of leaving the caller hanging."
      },
      "typeVersion": 1
    },
    {
      "id": "pub001-sticky-step4",
      "name": "Sticky Note: Step 4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1264,
        -528
      ],
      "parameters": {
        "width": 600,
        "height": 220,
        "content": "Step 4 \u2014 Notify Slack\n\nRuns in parallel after a successful response. Posts a short preview of the generated text to a Slack channel via an Incoming Webhook, so a human can see the output without polling the API."
      },
      "typeVersion": 1
    },
    {
      "id": "pub001-webhook",
      "name": "Receive request",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -800,
        0
      ],
      "parameters": {
        "path": "generate-with-claude",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "pub001-validate",
      "name": "Validate input & apply defaults",
      "type": "n8n-nodes-base.code",
      "position": [
        -560,
        0
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json || {};\nconst body = input.body || input;\n\nconst userPrompt = (body.userPrompt || '').toString().trim();\nif (!userPrompt) {\n  return [{ json: { validationError: true, errorMessage: 'userPrompt is required but was empty or missing.' } }];\n}\n\nconst systemPromptCaller = (body.systemPrompt || '').toString();\n// Lightweight guardrail applied to every call. Callers can still pass their own systemPrompt above this.\nconst commonGuardrail = '\\n\\n[Guardrail] Clearly separate facts from speculation. Do not state uncertain information as fact, and do not invent names, statistics, or sources that were not provided.';\nconst systemPrompt = (systemPromptCaller + commonGuardrail).trim();\n\nconst model = (body.model || 'claude-sonnet-5').toString();\nconst maxTokens = Number.isFinite(body.maxTokens) ? body.maxTokens : 4000;\nconst temperature = Number.isFinite(body.temperature) ? body.temperature : 1;\n\nreturn [{ json: { validationError: false, systemPrompt, userPrompt, model, maxTokens, temperature } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "pub001-if-valid",
      "name": "Input valid?",
      "type": "n8n-nodes-base.if",
      "position": [
        -320,
        0
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "pub001-cond-valid",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.validationError === false }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "pub001-respond-validation-error",
      "name": "Respond: validation error",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -64,
        352
      ],
      "parameters": {
        "options": {
          "responseCode": 400
        },
        "respondWith": "json",
        "responseBody": "={{ { success: false, error: $json.errorMessage } }}"
      },
      "typeVersion": 1.5
    },
    {
      "id": "pub001-build-request",
      "name": "Build Claude request body",
      "type": "n8n-nodes-base.code",
      "position": [
        -64,
        0
      ],
      "parameters": {
        "jsCode": "const d = $input.first().json;\n\nconst body = {\n  model: d.model,\n  max_tokens: d.maxTokens,\n  temperature: d.temperature,\n  messages: [\n    { role: 'user', content: d.userPrompt }\n  ]\n};\nif (d.systemPrompt) {\n  body.system = d.systemPrompt;\n}\n\nreturn [{ json: body }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "pub001-http-claude",
      "name": "Call Claude (Anthropic) API",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "No credential ID is embedded \u2014 assign your own Anthropic credential in n8n before activating.",
      "maxTries": 3,
      "position": [
        208,
        0
      ],
      "parameters": {
        "url": "https://api.anthropic.com/v1/messages",
        "method": "POST",
        "options": {
          "timeout": 60000
        },
        "jsonBody": "={{ $json }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            }
          ]
        },
        "nodeCredentialType": "anthropicApi"
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2,
      "continueOnFail": true,
      "waitBetweenTries": 2000
    },
    {
      "id": "pub001-if-success",
      "name": "Call succeeded?",
      "type": "n8n-nodes-base.if",
      "position": [
        448,
        0
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "pub001-cond-success",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.error ? 'error' : 'ok' }}",
              "rightValue": "ok"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "pub001-format-success",
      "name": "Format success response",
      "type": "n8n-nodes-base.code",
      "position": [
        704,
        -144
      ],
      "parameters": {
        "jsCode": "const resp = $input.first().json;\nconst textBlock = (resp.content || []).find(b => b.type === 'text');\nconst text = (textBlock && textBlock.text) || '';\n\nreturn [{\n  json: {\n    success: true,\n    text,\n    model: resp.model || null,\n    stopReason: resp.stop_reason || null,\n    usage: resp.usage || null\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "pub001-respond-success",
      "name": "Respond: generated text",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        960,
        -144
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        },
        "respondWith": "json",
        "responseBody": "={{ $json }}"
      },
      "typeVersion": 1.5
    },
    {
      "id": "pub001-set-slack-url",
      "name": "Set: Slack webhook URL",
      "type": "n8n-nodes-base.set",
      "notes": "Placeholder value \u2014 not a real webhook URL. Create your own Slack Incoming Webhook (Slack > your app > Incoming Webhooks) and paste the URL here before activating. No Slack app OAuth setup beyond that is required.",
      "position": [
        1264,
        -144
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg-slack-url",
              "name": "slackWebhookUrl",
              "type": "string",
              "value": "https://hooks.slack.com/services/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"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "pub001-build-slack-message",
      "name": "Build Slack notification body",
      "type": "n8n-nodes-base.code",
      "position": [
        1504,
        -144
      ],
      "parameters": {
        "jsCode": "const generated = $('Format success response').first().json;\nconst config = $input.first().json;\n\nconst preview = (generated.text || '').slice(0, 500);\n\nreturn [{\n  json: {\n    slackWebhookUrl: config.slackWebhookUrl,\n    slackRequestBody: { text: `New response generated via Claude:\\n${preview}` }\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "pub001-notify-slack",
      "name": "Notify Slack",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "continueOnFail=true so a placeholder or misconfigured Slack URL never affects the caller's response \u2014 the webhook has already responded independently by this point.",
      "position": [
        1760,
        -144
      ],
      "parameters": {
        "url": "={{ $json.slackWebhookUrl }}",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.slackRequestBody }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "pub001-format-error",
      "name": "Format upstream error response",
      "type": "n8n-nodes-base.code",
      "position": [
        704,
        304
      ],
      "parameters": {
        "jsCode": "const err = $input.first().json.error || $input.first().json;\nconst detail = typeof err === 'string' ? err : JSON.stringify(err);\nreturn [{ json: { success: false, error: 'Claude API call failed after retries.', detail } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "pub001-respond-error",
      "name": "Respond: upstream error",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        960,
        304
      ],
      "parameters": {
        "options": {
          "responseCode": 502
        },
        "respondWith": "json",
        "responseBody": "={{ $json }}"
      },
      "typeVersion": 1.5
    }
  ],
  "active": true,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "cef8a4b4-6213-4e3e-80e0-8e0c41638bce",
  "nodeGroups": [],
  "connections": {
    "Input valid?": {
      "main": [
        [
          {
            "node": "Build Claude request body",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond: validation error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call succeeded?": {
      "main": [
        [
          {
            "node": "Format success response",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format upstream error response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Receive request": {
      "main": [
        [
          {
            "node": "Validate input & apply defaults",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set: Slack webhook URL": {
      "main": [
        [
          {
            "node": "Build Slack notification body",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format success response": {
      "main": [
        [
          {
            "node": "Respond: generated text",
            "type": "main",
            "index": 0
          },
          {
            "node": "Set: Slack webhook URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Claude request body": {
      "main": [
        [
          {
            "node": "Call Claude (Anthropic) API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Claude (Anthropic) API": {
      "main": [
        [
          {
            "node": "Call succeeded?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Slack notification body": {
      "main": [
        [
          {
            "node": "Notify Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format upstream error response": {
      "main": [
        [
          {
            "node": "Respond: upstream error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate input & apply defaults": {
      "main": [
        [
          {
            "node": "Input valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}