n8n and Make template

Coolie + Slack: alert when a lead replies

This n8n workflow receives Coolie's lead.replied webhook, checks its signature and posts the reply to a Slack channel as it comes in: who answered, at which company, by email or on LinkedIn, the subject and the text, or a note that they asked to stop. Out-of-office answers and bounces are not replies, so they never trigger it. In Make, the same flow is a custom webhook, a filter and a Slack module.

Updated · Works with n8n, Make, Slack · Coolie event: lead.replied

What the workflow does

  1. Coolie webhook (Webhook)

    Receives Coolie's POST, answers at once, and keeps the raw body, which the signature is computed on.

  2. Verify Coolie signature (Code)

    Recomputes the Coolie-Signature header, an HMAC-SHA256 of the timestamp, a dot and the raw body with your webhook's secret, compares it in constant time, refuses a request older than five minutes, and passes the event on.

  3. Replies only (IF)

    Lets lead.replied events through, and Coolie's test event while you set it up.

  4. Post to Slack (Slack)

    Posts who answered and at which company, the channel, the subject and the text, or that they asked to stop.

Set it up in n8n

  1. Import the workflow

    Download the JSON file below. In n8n, open a new workflow and choose Import from File, or Import from URL with https://trycoolie.ai/templates/n8n/coolie-lead-replied-to-slack.json, or paste the JSON onto the canvas.

  2. Connect Slack

    Open Post to Slack, create or pick a Slack credential, and choose the channel.

  3. Copy the webhook's address

    Open Coolie webhook and copy its Production URL: the address Coolie will post to.

  4. Add the webhook in Coolie

    In Coolie, open Settings → Webhooks, paste the address, tick Lead replied (lead.replied) and click Add webhook: nothing is sent yet. Then click Reveal and copy the webhook's secret, which starts with whsec_.

  5. Paste the secret

    In n8n, open Verify Coolie signature and paste the secret between the quotes on its first lines. Without it, the node stops every event, with an error that says so.

  6. Publish the workflow

    Save, then publish the workflow (on n8n before version 2.0, switch it to active): its Production URL now answers.

  7. Send a test

    Back in Coolie, click Send test on the webhook: a test message appears in your channel.

coolie-lead-replied-to-slack.json

An n8n workflow (Webhook, Code, IF and the tools' own nodes), with a note on the canvas that repeats these steps.

Download the JSON file

Show the workflow JSON
coolie-lead-replied-to-slack.json
{
  "name": "Coolie: lead replied → Slack",
  "nodes": [
    {
      "parameters": {
        "content": "## Coolie: alert Slack when a lead replies\n\nWhen a lead answers one of your Coolie campaigns, by email or on LinkedIn, or asks it to stop, this workflow posts the reply to a Slack channel.\n\n### How it works\n1. **Coolie webhook** receives the **lead.replied** event, raw body kept.\n2. **Verify Coolie signature** checks the `Coolie-Signature` header with your webhook's secret and stops anything else.\n3. **Replies only** lets `lead.replied` events through, and Coolie's test event.\n4. **Post to Slack** sends who answered, at which company, on which channel, and their words.\n\n### Setup\n1. In **Post to Slack**, connect Slack and pick the channel.\n2. Copy the **Production URL** of **Coolie webhook**.\n3. In Coolie: **Settings → Webhooks**, paste the URL, tick **Lead replied**, click **Add webhook**, then **Reveal** the secret.\n4. Paste the secret at the top of **Verify Coolie signature**.\n5. Save and publish the workflow (on n8n before 2.0, activate it).\n6. Click **Send test** in Coolie: a test message appears in Slack.\n\nOut-of-office answers and bounces are not replies: they never reach this workflow.\nSelf-hosted n8n: allow the crypto module (`NODE_FUNCTION_ALLOW_BUILTIN=crypto`, on the task runners when you use them).\nGuide: https://trycoolie.ai/developers/templates/lead-replied-to-slack",
        "height": 700,
        "width": 480
      },
      "id": "a341ba3b-9d99-4bb6-889d-a04cbbb4d8cc",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -560,
        -280
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "coolie-lead-replied",
        "responseMode": "onReceived",
        "options": {
          "rawBody": true
        }
      },
      "id": "72b85a0f-2c28-4f24-8d2d-c8510ad6f032",
      "name": "Coolie webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        0
      ],
      "webhookId": "38deca36-b278-432a-8620-1100417e64f3"
    },
    {
      "parameters": {
        "jsCode": "// Coolie signs every event: Coolie-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of \"t.body\">.\n// This node recomputes it on the raw body with your webhook's secret, and stops anything else.\n// Paste the secret below: Coolie > Settings > Webhooks > Reveal (it starts with whsec_).\nconst SECRET = 'PASTE_YOUR_COOLIE_WEBHOOK_SECRET';\n\nconst crypto = require('crypto');\nif (!SECRET.startsWith('whsec_')) {\n  throw new Error('Paste your Coolie webhook secret (whsec_...) in the Verify Coolie signature node.');\n}\nconst request = $input.first().json;\nconst rawBody = (await this.helpers.getBinaryDataBuffer(0, 'data')).toString('utf8');\nconst parts = Object.fromEntries(\n  String(request.headers['coolie-signature'] || '').split(',').map((part) => part.split('=')),\n);\nconst expected = Buffer.from(crypto.createHmac('sha256', SECRET).update(parts.t + '.' + rawBody).digest('hex'));\nconst given = Buffer.from(String(parts.v1 || ''));\nconst fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;\nif (!fresh || given.length !== expected.length || !crypto.timingSafeEqual(given, expected)) {\n  throw new Error('Not a Coolie event: the signature does not match.');\n}\nreturn [{ json: JSON.parse(rawBody), pairedItem: { item: 0 } }];"
      },
      "id": "b44bcfef-1a74-479a-86fa-e21cd1b52de7",
      "name": "Verify Coolie signature",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "551a68cb-2f6d-42dc-8e2c-2885b45f56ea",
              "leftValue": "={{ $json.type }}",
              "rightValue": "lead.replied",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            },
            {
              "id": "fe1d48a2-ce4b-4707-85db-802e5702a238",
              "leftValue": "={{ $json.type }}",
              "rightValue": "test",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "or"
        },
        "options": {}
      },
      "id": "ef225f27-2093-443f-8d85-70c26706b09a",
      "name": "Replies only",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        480,
        0
      ]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "text": "=:speech_balloon: *{{ $json.type === 'test' ? 'Test event from Coolie' : ($json.data.reply.kind === 'stop' ? 'A lead asked to stop' : 'A lead replied by ' + $json.data.reply.channel) }}*\n{{ $json.data.lead && $json.data.lead.contact ? $json.data.lead.contact.name + ($json.data.lead.contact.title ? ', ' + $json.data.lead.contact.title : '') : 'Lead' }}{{ $json.data.lead ? ' at ' + $json.data.lead.company.name : '' }}\n{{ $json.data.reply && $json.data.reply.subject ? '*' + $json.data.reply.subject + '*' : '' }}\n{{ $json.data.reply ? ($json.data.reply.text || '(no text)') : $json.data.message }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "id": "d68d1525-f16c-4ab4-8765-28e7e0303d5c",
      "name": "Post to Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        720,
        0
      ]
    }
  ],
  "connections": {
    "Coolie webhook": {
      "main": [
        [
          {
            "node": "Verify Coolie signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Coolie signature": {
      "main": [
        [
          {
            "node": "Replies only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Replies only": {
      "main": [
        [
          {
            "node": "Post to Slack",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  }
}

Build it in Make

Make's scenario files are its own, so here is the same flow, module by module, in a new scenario.

  1. Create the webhook in Make

    In a new scenario, add Webhooks › Custom webhook, click Add, name it (Coolie leads) and copy its address. Make then waits for a first request to learn the data's structure.

  2. Send Coolie's test event

    In Coolie, open Settings → Webhooks, paste the address, tick Lead replied (lead.replied), click Add webhook, then Send test. Make shows that it determined the structure from the test event, which carries a sample lead in the real shape.

  3. Teach Make the reply's fields

    The test event carries a lead but no reply. To map the reply before the first real one, give the webhook a data structure generated from the sample below, or redetermine the structure when a first reply arrives.

  4. Add the Slack message

    Add Slack › Create a Message, choose the channel, and write the text from data › lead › contact › name, data › lead › company › name, and data › reply › kind, channel, subject and text.

  5. Filter on replies

    On the link between the two modules, set up a filter: type Equal to lead.replied.

  6. Turn the scenario on

    Save, and switch the scenario on, set to run immediately as data arrives.

  7. Optional: check Coolie's signature

    A Make webhook address is long and random: keep it private. To refuse anything Coolie did not sign, turn on Get request headers and JSON pass-through in the webhook's advanced settings, parse the text it passes with JSON › Parse JSON, and add a filter comparing the v1 part of the Coolie-Signature header with Make's sha256 function given your secret as its key (it then computes an HMAC, in hex), over the t part of the header, a dot and the raw body.

A lead.replied event

The lead, as GET /api/v1/leads gives it (null if it was deleted since), and the reply: kind reply or stop, channel email or linkedin, its subject, its text and when it came in. The values are examples.

JSON
{
  "id": "evt_Lm5pW0qZ8rTc3XyA",
  "type": "lead.replied",
  "createdAt": "2026-10-21T14:07:31.502Z",
  "data": {
    "lead": {
      "runId": "ui-5b9e2c41",
      "prospectId": "p-41b7c2d9e0aa",
      "agentId": "agent-3f2a91c0",
      "status": "written",
      "processedAt": "2026-10-19T09:02:13.511Z",
      "icpMatch": true,
      "aiScore": 8.4,
      "feedback": null,
      "signal": {
        "type": "hiring",
        "name": "Hiring",
        "headline": "Operations Manager",
        "detail": null,
        "url": "https://jobs.example.com/12345",
        "postedAt": "2026-10-17"
      },
      "company": {
        "name": "Northwind Health",
        "domain": "northwind.example",
        "website": "https://northwind.example",
        "linkedinUrl": "https://www.linkedin.com/company/example",
        "industry": "Hospitals and Health Care",
        "employees": 320,
        "country": "France",
        "locations": [
          "Lyon, France"
        ]
      },
      "contact": {
        "name": "Camille Martin",
        "title": "COO",
        "seniority": "c_suite",
        "linkedinUrl": "https://www.linkedin.com/in/example",
        "country": "France",
        "email": "camille.martin@northwind.example",
        "emailConfidence": 0.92,
        "phone": null
      },
      "research": null,
      "sequence": null
    },
    "reply": {
      "campaignId": "cmp-4e1a9c7b2d80",
      "queueItemId": "q-8b2f6d0e9a13",
      "kind": "reply",
      "channel": "email",
      "subject": "Re: Two clinics, one schedule",
      "text": "Hi, good timing. Could we talk on Thursday afternoon?",
      "receivedAt": "2026-10-21T14:05:12.000Z"
    }
  }
}

Frequently asked questions

Which answers trigger the alert?

A reply to one of your campaign's emails or LinkedIn messages, and a request to stop (kind stop). Out-of-office answers and bounces do not.

Does the campaign stop when the lead replies?

When its sending rules pause on reply, the lead's next steps stop. This alert only tells your team; Coolie's inbox holds the whole conversation.

Is the lead always in the event?

Yes, unless it was deleted since: data.lead is then null, and the reply is still told.

Does Coolie have a Slack app?

No. This template connects the two through Coolie's signed webhooks: Coolie posts each event to n8n or Make, which posts to Slack. The same events can feed any tool n8n or Make reaches.

My self-hosted n8n refuses require('crypto'). What do I do?

The Code node that checks the signature needs Node's crypto module. n8n Cloud allows it; on a self-hosted n8n, set NODE_FUNCTION_ALLOW_BUILTIN=crypto (on the task runners when you use them) and restart.

How the events are signed and retried: receive Coolie webhooks and verify their signature. Every template: Coolie workflow templates.

Your next conversation is already happening somewhere.

Set up an agent in five minutes. The first leads arrive with their reason to write.