Use Slack's event_id to make Notion triage replay-safe

Use Slack's event_id to make Notion triage replay-safe

Turn one chosen Slack reaction into an auditable Notion signal page, using event_id, a durable lock, and create-or-patch branches to handle delivery retries safely.

The trick: make one Slack reaction become one replay-safe Notion signal

Pick one emoji, such as eyes, as the team's "triage this" mark. When someone adds that reaction to a Slack message, create one Notion page keyed by Slack's event_id. When Slack delivers the same event again, find that page and update its delivery timestamp instead of creating a second task.
Slack wraps every Events API callback with a globally unique event_id. A reaction_added callback also contains the reaction name, the actor, and a lightweight reference to the reacted-to message: its channel and timestamp. 12
The workflow has a narrow job:
Slack reaction_added
  -> verify and acknowledge the event
  -> acquire a lock for event_id
  -> query Notion by Event ID
  -> create the signal page, or PATCH the existing page
  -> release the lock
The result is an auditable intake ledger. A PM can react to a customer quote, an experiment result, or a launch concern; the Notion row retains the precise Slack coordinates that triggered it. A retry only refreshes the same row.

Before you build it

Prepare these pieces before opening n8n:
  • A Slack app subscribed to reaction_added. The event requires the reactions:read scope. The event payload may describe a message, a file, or a file comment, so this workflow accepts only item.type = message. 2
  • An n8n workflow that receives the Slack event. Use the Slack Trigger when it fits your deployment, or send a verified Events API request into an n8n webhook. Keep the Slack signing secret and Notion token in credentials or environment variables, never in node fields that become workflow data.
  • A Notion connection shared with the target data source. Querying requires read-content capability and access to the parent database. Creating a page requires insert-content capability; patching a page requires update-content capability. 345
  • A durable per-event lock. Use your team's existing Redis, Postgres, or queue layer to make a conditional lock on event_id. The lock closes the gap between a Notion query and page creation.
Use the current Notion header on every HTTP Request node:
Authorization: Bearer $NOTION_API_KEY
Notion-Version: 2026-03-11
Content-Type: application/json
Notion's current documentation lists 2026-03-11 as the latest version for querying data sources and creating or updating pages. 345

Set up the Notion signal ledger

Create a data source called PM Signals. These fields keep the event searchable without asking the automation to guess the message's meaning.
PropertyTypeValue from SlackWhy it stays
SignalTitleSlack signal: {{event.reaction}}A scannable row name.
Event IDRich textevent_idThe replay key.
Channel IDRich textevent.item.channelThe original Slack location.
Message TSRich textevent.item.tsThe original message coordinate.
ReactionSelectevent.reactionLets a view separate eyes from other approved marks.
Reactor IDRich textevent.userShows who raised the signal.
Received atDateevent_time converted to ISO 8601Records delivery time.
StatusStatusInboxLeaves prioritization to a human.
Last delivery atDateCurrent workflow timeMakes a retry visible without making a new row.
Share the parent database with the Notion connection. A data-source query against an unshared parent returns 404, which can look like a bad ID during setup. 3

Build the workflow in n8n

1. Accept only the signal you mean to collect

Subscribe the Slack app to reaction_added, then add a filter immediately after the trigger:
event.type       == "reaction_added"
event.item.type  == "message"
event.reaction   == "eyes"
The reaction_added event uses event.user for the reacting user and event.item for the reacted-to object. Slack's message item contains channel and ts; it does not carry the full message body. 2
Build one normalized object for every accepted event:
{
  "eventId": "{{$json.event_id}}",
  "channelId": "{{$json.event.item.channel}}",
  "messageTs": "{{$json.event.item.ts}}",
  "reaction": "{{$json.event.reaction}}",
  "reactorId": "{{$json.event.user}}",
  "receivedAt": "{{$json.event_time}}"
}
Keep eventId unchanged. The event wrapper identifies it as unique across Slack workspaces, so adding your own channel or timestamp to that key only makes replay debugging harder. 1

2. Verify first, acknowledge fast, then hand off

A public Events API endpoint must verify Slack's signature from the raw request body. Slack signs a base string containing the version, X-Slack-Request-Timestamp, and raw body; Slack's guide also recommends rejecting a timestamp more than five minutes from local time. 6
Return an HTTP 2xx response before the workflow does Notion work. Slack expects that response within three seconds and retries failed deliveries three times with exponential backoff. Slack specifically recommends queuing inbound work after acknowledgement. 1
A practical n8n shape is a tiny verified receiver that writes the normalized event to your queue, followed by a worker workflow that performs the remaining steps. When the Slack Trigger handles verification for your deployment, retain the same split: event receipt ends quickly; Notion work runs after the acknowledgement boundary.

3. Lock the event ID

Before the Notion query, acquire a lock whose key is:
slack-event:{{eventId}}
Set a short expiry longer than the worker's worst-case Notion retry window. A lock acquisition failure means another execution already owns this delivery; end the current execution successfully.
This step matters because Slack can retry a request while the first execution is still querying Notion. Query -> zero results -> create is two API calls, and Notion does not turn them into an atomic upsert. The lock gives the workflow one writer for one Slack delivery.

4. Query Notion by Event ID

Add an HTTP Request node with this method and URL:
POST https://api.notion.com/v1/data_sources/DATA_SOURCE_ID/query?filter_properties[]=Event%20ID&filter_properties[]=Last%20delivery%20at
Use this JSON body. Substitute the normalized event ID through an n8n expression.
{
  "filter": {
    "property": "Event ID",
    "rich_text": {
      "equals": "{{$json.eventId}}"
    }
  },
  "page_size": 2
}
POST /v1/data_sources/{data_source_id}/query returns pages filtered and ordered by the request. The filter_properties parameter can limit the properties returned, which keeps a duplicate check small on a data source with formulas or rollups. 3
Branch on the number of returned pages:
  • 0 pages: create a signal page.
  • 1 page: patch Last delivery at on that page.
  • 2 pages: stop and alert the workflow owner. Two rows with the same event ID are data corruption, not a condition to resolve by picking the newest row.

5. Create the page on the first delivery

For the zero-result branch, configure a second HTTP Request node:
POST https://api.notion.com/v1/pages
{
  "parent": {
    "type": "data_source_id",
    "data_source_id": "DATA_SOURCE_ID"
  },
  "properties": {
    "Signal": {
      "title": [{ "text": { "content": "Slack signal: eyes" } }]
    },
    "Event ID": {
      "rich_text": [{ "text": { "content": "{{$json.eventId}}" } }]
    },
    "Channel ID": {
      "rich_text": [{ "text": { "content": "{{$json.channelId}}" } }]
    },
    "Message TS": {
      "rich_text": [{ "text": { "content": "{{$json.messageTs}}" } }]
    },
    "Reaction": {
      "select": { "name": "eyes" }
    },
    "Reactor ID": {
      "rich_text": [{ "text": { "content": "{{$json.reactorId}}" } }]
    },
    "Status": {
      "status": { "name": "Inbox" }
    },
    "Last delivery at": {
      "date": { "start": "{{$now.toISO()}}" }
    }
  }
}
A page created beneath a data source must use property keys that match that data source's schema. The Create Page API supports data_source_id as the parent. 4
Use a Notion template only after the ledger works. A default or named template can populate a newly created page, but the template applies asynchronously after the API call. 4

6. Patch the existing page on a replay

For the one-result branch, use the returned page ID:
PATCH https://api.notion.com/v1/pages/{{results[0].id}}
{
  "properties": {
    "Last delivery at": {
      "date": { "start": "{{$now.toISO()}}" }
    }
  }
}
The Update Page API modifies properties on a page whose parent is a data source, provided the property schema matches the parent and the connection has update-content capability. 5
Release the eventId lock after either the create or patch path. Keep the lock on a retryable Notion error until its expiry, then let Slack's next delivery or your queue retry policy handle the event. A failed execution should never write a blank page merely to mark an event handled.

Test the behavior you need

  1. Add eyes to one ordinary channel message. Expect one PM Signals page with the Slack event ID, channel ID, message timestamp, reaction, and Inbox status.
  2. Replay the same payload through a non-production n8n test path. Expect one page and a newer Last delivery at, with no second row.
  3. Add a different emoji to a message. Expect no workflow write.
  4. Send a reaction_added event whose item.type is a file. Expect the filter to stop it before the Notion calls.
  5. Remove the data source from the Notion connection's shared resources. Expect a clear access failure, then restore the share. A 404 from the query path can mean the connection lacks access to the parent database. 3

Gotchas

Do not verify a parsed payload. The HMAC uses the raw body. Preserve it before JSON parsing, and keep the Slack signing secret separate from the Notion bearer token. 6
Do not make Notion your first acknowledgement step. A slow query, an unavailable connection, or an n8n queue backlog can exceed Slack's three-second response window and trigger retries. Reply first and process the normalized event afterward. 1
Keep Event ID as rich text, not a title. A title field is for the human-readable signal. A separate rich-text field gives the query one exact, machine-owned key.
Treat two matching rows as an incident. The Notion query can filter precisely, but it is not a uniqueness constraint. The durable lock and the 2 pages stop branch prevent an automation failure from becoming a silent backlog-cleanup problem. 3
Fetch message text only when it earns its scope. The reaction event gives you coordinates, not the original text. Start with the ledger and the Slack link your team already uses. Add a message-lookup call only after the Slack app has the least privilege needed to read that channel.

Este contenido lo produjo un canal automáticamente. Con una sola frase, Neodrop puede seguir produciendo para ti.

Contenido relacionado

More from this channel