DEV Community

mumuzhang
mumuzhang

Posted on

Route app errors to the right Slack channel

The problem

I kept hitting the same friction: rebuilding the same automation from scratch every time a slightly different event showed up. Copying a previous project helped for about an hour, and then the small differences took over: different payload shapes, different failure modes, different places to look when nothing arrived. So I built 2026-10-28_error-alert-router to make the whole thing repeatable instead of memorable.

How it works

  1. Webhook (webhook) — accepts the inbound event over HTTP and is the only entry point into the workflow. Keeping exactly one entry point is what makes the rest of the pipeline testable, because every execution starts from a request you can replay. Without it, events arrive through side doors and you end up debugging a shape you never designed for.
  2. HMAC verify (function) — verifies the request signature before any of the body is trusted. This runs before parsing on purpose: an unsigned or badly signed body should never reach code that might act on it. Skipping it on "internal" traffic stops being safe the moment someone copies the URL into a second service.
  3. Idempotency check (if) — performs its step in the pipeline and passes the result forward. It is deliberately narrow, which keeps the failure domain small: when something breaks, the node name already tells you which of the four things went wrong.
  4. Deliver to destination (httpRequest) — delivers the normalized record to its destination. The payload is whatever the previous step produced, which means the destination sees one consistent shape no matter which source sent the event. Timeouts and non-2xx responses are treated as failures rather than soft successes, so retries do not silently pile up.
  5. Error branch (noOp) — collects failures so they surface instead of disappearing. An error branch that only appends to a file nobody opens is not an error branch; it has to end somewhere a human actually looks. This is also where partial successes land, which is how you find the case that half-worked.

The order matters more than the individual pieces. Verifying before parsing means a malformed or hostile body never reaches the part of the system that would act on it. The idempotency guard is the piece I used to skip, and it is the piece that actually saves time: once duplicates are dropped at the door, every downstream failure is a real failure instead of noise.

The code

The structure below is taken directly from the generated artifact, not rewritten for the post:

{
  "name": "Route app errors to the right Slack channel",
  "nodes": [
    {
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1.1
    },
    {
      "name": "HMAC verify",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1
    },
    {
      "name": "Idempotency check",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1
    },
    {
      "name": "Deliver to destination",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2
    }
  ],
  "connections": [
    "Webhook",
    "HMAC verify",
    "Idempotency check",
    "Deliver to destination"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Nothing here is clever on purpose. Each node does one thing, and the thing it does is readable from its name.

What broke

The first version passed every happy-path test and then lost events in production for two reasons that only show up under load. The first was treating a signature check as optional on internal traffic — it is not optional, because "internal" stops being true the moment someone copies the webhook URL into a second service. The second was writing failures to a log nobody read. An error branch that only appends to a file is not an error branch; it has to end somewhere a human looks.

What I'd do differently

Start smaller. The first version had features I could not explain to myself a week later, and every one of them became a place for a silent failure to hide. Cutting it down to trigger, verify, guard, deliver made the whole thing easier to trust, and trust is the actual feature — the automation is only useful if you stop checking it. I would also write the idempotency key format down before writing code, because retrofitting a key scheme onto events already in flight is the least fun kind of migration.

Full workflow and setup live in the GitHub repo.

What the project actually does

Audience: operators who receive "error-alert-router" events and need them actioned without manual copy-paste.
How it works: a webhook captures the event, an HMAC check authenticates it, an idempotency guard drops duplicates, then the payload is delivered to your destination endpoint.
Setup: import the JSON, set the HMAC secret in the marked node, connect the destination service, activate.
Services: Slack or Google Sheets depending on the variant; the webhook accepts any JSON payload.
Extend: add rate limiting, retry queues, or a second destination in parallel.

  1. In n8n open Workflows → Import from File and select workflow.json.
  2. Open the HMAC Verify node and set HMAC to your shared secret.
  3. Open the delivery node and set the destination URL (Slack / Notion / Sheets).
  4. Activate the workflow and copy the production webhook URL.

Duplicate deliveries of the same id are dropped by the idempotency guard.

Sticky notes are embedded in the workflow and mirrored in sticky_notes.md.

Production version with docs & support: WAFFO=

Top comments (0)