How to Capture Application Error Webhooks and Deduplicate Noisy Alerts
Receive application error events over a Custom-scheme HMAC webhook, fingerprint each error with the encoding connector, suppress repeats that are already known in MongoDB, and escalate only genuinely new errors to Slack.
What This Integration Does
When an application starts failing, the same error often fires hundreds of times a minute. If every event becomes a Slack message, the channel turns into noise and on-call engineers tune it out. This Spojit workflow gives you a single front door for error events: your app (or any error-reporting tool) POSTs each error to a webhook, Spojit computes a stable fingerprint for it, and only the first occurrence of a given fingerprint inside your chosen time window reaches Slack. Every later repeat quietly increments a count on the existing record instead of pinging the channel again.
The workflow runs once per inbound POST. It is triggered by a Webhook trigger verified with a Custom HMAC signing connection, so only requests signed with your shared secret are accepted. Each run hashes the error to a deterministic fingerprint, looks that fingerprint up in a MongoDB error_fingerprints collection, and branches: if no recent record exists it inserts one and posts to Slack, otherwise it bumps the count and lastSeen on the existing record and exits silently. The collection is the durable state that makes re-runs idempotent: replaying the same event never produces a duplicate alert.
Prerequisites
- A MongoDB connection (database reachable from Spojit) with a collection named
error_fingerprints. A unique index on thefingerprintfield is recommended. - A Slack connection authorized to post to your alerts channel, and the channel ID or name you want errors sent to (for example
#prod-alerts). - A Webhook signing connection using the Custom scheme, holding the shared HMAC secret. See Setting Up a Webhook Connection.
- The error-reporting side (your app or monitoring tool) must be able to send an HTTP POST with a JSON body and an HMAC signature header.
- The encoding connector is built in and needs no connection.
Step 1: Add a Webhook trigger with Custom HMAC verification
Create a new workflow and set its trigger to Webhook. Under the trigger, choose your Custom signing connection so Spojit verifies the HMAC signature on every request and rejects anything unsigned or tampered with. Spojit returns 202 with an executionId as soon as the body is accepted, so your sender does not wait for the workflow to finish.
The trigger output is the parsed JSON body, available as {{ input }}. Have your application POST a consistent shape such as:
{
"service": "checkout-api",
"environment": "production",
"errorType": "TimeoutError",
"message": "Upstream payment gateway timed out after 30s",
"stackHead": "at PaymentClient.charge (payment.js:212)"
}
For background on configuring this trigger, see Setting Up a Webhook Trigger.
Step 2: Build a stable fingerprint string with a Transform node
Add a Transform node to assemble the fields that define "the same error". Deliberately exclude volatile values such as timestamps, request IDs, or line-by-line stack traces, otherwise every event looks unique and nothing ever deduplicates. A good fingerprint joins the service, environment, error type, and a normalized message or top stack frame. Produce a single string, for example output variable fingerprintInput:
{{ input.service }}|{{ input.environment }}|{{ input.errorType }}|{{ input.stackHead }}
Keeping this string narrow and deterministic is what controls how aggressively repeats collapse together. See Using Transform Nodes (Plaintext Mode).
Step 3: Hash the fingerprint with the encoding connector
Add a Connector node in Direct mode on the encoding connector and pick the hash-sha256 tool. Map the text field to your assembled string and leave encoding as hex:
text: {{ fingerprintInput }}
encoding: hex
The tool returns a deterministic digest under hash (with algorithm and encoding alongside). Name the output variable fp so the fingerprint is available downstream as {{ fp.hash }}. The same input always produces the same hash, which is exactly what lets MongoDB recognize a repeat. For the field-by-field reference, see Using Connector Nodes in Direct Mode.
Step 4: Look up the fingerprint in MongoDB within a time window
Add a Connector node in Direct mode on the MongoDB connector and choose find-documents. Set collection to error_fingerprints and use a filter that matches both the fingerprint and a "still recent" cutoff, so an alert can fire again after the noise window expires. Compute the cutoff with a Transform node (or the date connector) and reference it here as {{ windowStart }} (an ISO timestamp, for example one hour ago):
{
"fingerprint": "{{ fp.hash }}",
"lastSeen": { "$gte": "{{ windowStart }}" }
}
Set limit to 1. The tool returns { success, data: { documents, count } }. Name the output variable lookup so you can branch on {{ lookup.data.count }}: 0 means this is a new error within the window, and 1 means a record already exists.
Step 5: Branch on whether the error is new with a Condition node
Add a Condition node that tests whether {{ lookup.data.count }} equals 0. The true branch handles a genuinely new error (record it, then alert), and the false branch handles a repeat (just increment the count, no Slack message). This single branch is what keeps your channel quiet during an incident storm while still capturing the full volume in the database. For comparison operators and nested templates, see Using Condition Nodes.
Step 6: New error - record it and post to Slack
On the true branch, first add a Connector node in Direct mode on the MongoDB connector using update-documents with upsert set to true. This atomically creates the record (or revives an expired one) and seeds its counters in one call:
collection: error_fingerprints
filter: { "fingerprint": "{{ fp.hash }}" }
update: {
"$set": {
"fingerprint": "{{ fp.hash }}",
"service": "{{ input.service }}",
"environment": "{{ input.environment }}",
"errorType": "{{ input.errorType }}",
"message": "{{ input.message }}",
"lastSeen": "{{ input.receivedAt }}"
},
"$inc": { "count": 1 }
}
upsert: true
Then add a Connector node in Direct mode on the Slack connector with send-message. Set the channel to your alerts channel and write a concise text that names the service and the fingerprint so engineers can correlate it back to the record:
channel: #prod-alerts
text: ":rotating_light: New error in {{ input.service }} ({{ input.environment }})\n{{ input.errorType }}: {{ input.message }}\nfingerprint: {{ fp.hash }}"
Step 7: Repeat error - increment the count silently
On the false branch, add a single Connector node in Direct mode on the MongoDB connector using update-documents (no upsert needed, the record already exists). Increment the running count and refresh lastSeen so the time window keeps sliding while the error is still active:
collection: error_fingerprints
filter: { "fingerprint": "{{ fp.hash }}" }
update: {
"$inc": { "count": 1 },
"$set": { "lastSeen": "{{ input.receivedAt }}" }
}
This branch sends nothing to Slack. The result is one alert per distinct error per window, with an accurate occurrence count you can read straight from MongoDB during triage.
Tips
- Tune the window by changing how far back
{{ windowStart }}reaches. A shorter window re-alerts sooner; a longer one stays quieter during a long incident. - Add the count and last-seen time into the Slack message on the true branch (for example reading
{{ lookup.data.documents }}on a revived record) so responders see whether an error is brand new or returning. - If you want to escalate only after a threshold, add a Condition on the repeat branch that posts to Slack once
countcrosses a number such as 100. - Ask Miraxa, the intelligent layer across your automation, to scaffold this for you: "Add a Condition node that checks if
{{ lookup.data.count }}is 0 and connect the true branch to a Slack send-message node," then fine-tune fields in the properties panel.
Common Pitfalls
- Including volatile data (timestamps, request IDs, full stack traces) in the fingerprint string makes every event unique, so nothing ever deduplicates. Keep
fingerprintInputto stable fields only. - Forgetting the
lastSeencutoff in thefind-documentsfilter means an old, long-resolved error never alerts again even after it returns weeks later. Always combine fingerprint plus a window. - Omitting
upsert: trueon the new-error branch creates a race where two near-simultaneous first events both find nothing and both alert. Upsert plus a unique index onfingerprintkeeps it idempotent. - An unsigned or wrongly signed POST is rejected by the Custom HMAC verification. If events never arrive, confirm the sender uses the same shared secret as the signing connection and signs the exact raw body.
Testing
Before pointing production traffic at the webhook, send a few signed test POSTs by hand with the same body twice in a row. The first should insert a record and produce exactly one Slack message; the second, identical body should find the existing record and post nothing while the count climbs to 2. Then send a body with a different errorType and confirm it alerts as a separate error. Inspect each run in the execution history to verify the fingerprint, the lookup count, and which branch ran, and check the error_fingerprints collection to confirm the counters look right. Once the dedup behavior is correct on this small scope, wire it into your real error stream.