AutomationFlowsAI & RAG › Generate Text via a Validated Webhook with Anthropic Claude and Slack

Generate Text via a Validated Webhook with Anthropic Claude and Slack

By鈴木光輝 @koki-suzuki on n8n.io

This workflow exposes a POST webhook that validates prompt inputs, calls the Anthropic Claude Messages API with retries, returns a consistent JSON response, and posts a preview of the generated text to Slack using an Incoming Webhook. Receives a POST request on a webhook…

Webhook trigger★★★★☆ complexity19 nodesHTTP Request
AI & RAG Trigger: Webhook Nodes: 19 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #17993 — we link there as the canonical source.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "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
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow exposes a POST webhook that validates prompt inputs, calls the Anthropic Claude Messages API with retries, returns a consistent JSON response, and posts a preview of the generated text to Slack using an Incoming Webhook. Receives a POST request on a webhook…

Source: https://n8n.io/workflows/17993/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

This workflow automates end-to-end social media publishing powered by Late API. It generates text content with Google Gemini, creates branded visuals with Kie.ai, uploads media to Late, and publishes

HTTP Request
AI & RAG

This workflow is perfect for app developers, SaaS founders, and mobile growth teams who need constant UGC-style video ads without hiring creators or agencies. If you're spending $500+ per creator and

HTTP Request, Google Drive
AI & RAG

AI Background Generation with Nano Banana (Gemini Image). Uses httpRequest, googleDrive. Webhook trigger; 35 nodes.

HTTP Request, Google Drive
AI & RAG

This template is for developers, teams, and automation enthusiasts who want a private, PIN-protected Telegram chatbot that answers questions from their own documents — without relying on external AI A

Postgres, Telegram, HTTP Request +2
AI & RAG

Elevate your digital presence with high-fidelity cinematic video automation. This workflow orchestrates the complex, asynchronous rendering process of OpenAI Sora—transforming static product images or

HTTP Request, N8N Nodes Uploadtourl