{
  "id": "DzmBIGjBrkYoq5K3",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Route human-in-the-loop approval requests with Slack and Postgres",
  "tags": [],
  "nodes": [
    {
      "id": "36b1962e-d20c-4b3b-85b7-c8ac34dd798b",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1968,
        1328
      ],
      "parameters": {
        "width": 480,
        "height": 800,
        "content": "## Route human-in-the-loop approval requests with Slack and Postgres\n\n### How it works\n\nThis workflow exposes a single webhook that handles both new human-in-the-loop intake requests and Slack interactive callbacks. It validates Slack signatures, routes incoming requests by type, sends approval messages to Slack, records request state in Postgres, and later processes button clicks by acknowledging Slack quickly, updating the original message, and storing the final decision.\n\n### Setup steps\n\n- Configure the webhook URL in Slack for interactive components and any external system that submits new HITL intake requests.\n- Add Slack credentials with permission to post and update messages, and configure the Slack signing secret used by the signature validation code.\n- Configure Postgres credentials and create the tables/columns expected by the request logging, context loading, and decision update nodes.\n- Review the code nodes for expected payload fields, Slack channel IDs, callback/action IDs, and database key names before activating the workflow.\n\n### Customization\n\nAdjust the Block Kit builder nodes to change the approval message layout, buttons, or metadata. Extend the final action switch to trigger different downstream automations for approve, reject, or other custom decisions."
      },
      "typeVersion": 1
    },
    {
      "id": "ebe68106-4569-4137-8795-3d5bc2614c1f",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2496,
        1424
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 368,
        "content": "## Receive and verify request\n\nAccepts all incoming traffic through the shared webhook and validates the Slack signature before allowing the request to continue."
      },
      "typeVersion": 1
    },
    {
      "id": "4c3ec1c7-450d-40dc-a0e0-b6efe35a0e68",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2944,
        1424
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 368,
        "content": "## Route request type\n\nIdentifies whether the verified request is a new intake submission or a Slack callback, then sends it to the appropriate branch."
      },
      "typeVersion": 1
    },
    {
      "id": "d675b7d9-fab6-4133-bca4-dedc03150b08",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3392,
        1328
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 352,
        "content": "## Send intake message\n\nBuilds the initial Slack Block Kit approval message, posts it to Slack, and validates the Slack API response."
      },
      "typeVersion": 1
    },
    {
      "id": "d9013b26-82a7-4236-ba8b-edb231706a75",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4064,
        1328
      ],
      "parameters": {
        "color": 7,
        "width": 688,
        "height": 352,
        "content": "## Store intake result\n\nLogs the newly posted request in Postgres and returns a successful webhook response confirming the intake was received."
      },
      "typeVersion": 1
    },
    {
      "id": "48ecffb3-0ea7-439c-acf8-f51787201a3b",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3392,
        1712
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 368,
        "content": "## Acknowledge Slack callback\n\nParses the Slack interactive callback, immediately responds with a 200 OK to satisfy Slack timing requirements, and loads the saved request context from Postgres."
      },
      "typeVersion": 1
    },
    {
      "id": "b44e20d9-6e2b-4e1a-b96f-19cf4e228988",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4064,
        1712
      ],
      "parameters": {
        "color": 7,
        "width": 688,
        "height": 368,
        "content": "## Update Slack decision message\n\nCombines callback details with database context, builds an updated Slack message, and removes the decision buttons from the original message."
      },
      "typeVersion": 1
    },
    {
      "id": "3a980328-14d9-4987-8249-673771253747",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4784,
        1712
      ],
      "parameters": {
        "color": 7,
        "width": 528,
        "height": 368,
        "content": "## Record and branch decision\n\nPersists the selected approval decision in Postgres and prepares the workflow to branch based on the action taken."
      },
      "typeVersion": 1
    },
    {
      "id": "a7c21d99-fb0f-4859-911e-f48c9281434c",
      "name": "Receive Slack POST Request",
      "type": "n8n-nodes-base.webhook",
      "position": [
        2544,
        1616
      ],
      "parameters": {
        "path": "slack-hitl-router",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "fa57f94a-e448-4d69-99d1-aa632dbbb0fc",
      "name": "Determine Slack Request Type",
      "type": "n8n-nodes-base.code",
      "position": [
        2992,
        1616
      ],
      "parameters": {
        "jsCode": "// ============================================================\n// ROUTER \u2014 Identify Request Type\n// Slack interactivity callbacks always contain a URL-encoded\n// 'payload' field in the POST body. Intake requests use your\n// own field structure. This node reads the signature and stamps\n// routeType so the Switch node can branch cleanly.\n// ============================================================\n\nconst body = $json.body ?? $json;\n\n// Slack sends callback as URL-encoded payload string\nconst isSlackCallback = !!body.payload;\n\nif (isSlackCallback) {\n  // Parse the URL-encoded payload string into a JS object\n  let slackPayload;\n  try {\n    const decoded = decodeURIComponent(body.payload);\n    slackPayload = JSON.parse(decoded);\n  } catch (e) {\n    throw new Error(`Failed to parse Slack payload: ${e.message}`);\n  }\n\n  return [{\n    json: {\n      routeType: 'slack_callback',\n      slackPayload,\n      rawBody: body\n    }\n  }];\n}\n\n// Intake request \u2014 normalize field names defensively\nconst requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;\n\nreturn [{\n  json: {\n    routeType: 'intake',\n    requestId,\n    requestType: body.requestType ?? body.type ?? 'general',\n    submittedBy: body.submittedBy ?? body.email ?? 'unknown',\n    subject:     body.subject ?? body.title ?? 'Approval Request',\n    details:     body.details ?? body.description ?? '',\n    amount:      body.amount ?? null,\n    metadata:    body.metadata ?? {},\n    channel:     body.channel ?? body.slackChannel ?? null,\n    rawBody:     body\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "f8ef4f4c-70b6-4aa1-9dd0-fe65700fe070",
      "name": "Route by Request Type",
      "type": "n8n-nodes-base.if",
      "position": [
        3216,
        1616
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-route",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.routeType }}",
              "rightValue": "intake"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "192909f8-cabb-4e81-b004-f28db97e3268",
      "name": "Create Slack Block Message",
      "type": "n8n-nodes-base.code",
      "position": [
        3440,
        1488
      ],
      "parameters": {
        "jsCode": "// ============================================================\n// BUILD BLOCK KIT MESSAGE\n// Constructs the full Slack Block Kit JSON for the approval\n// request. Context is embedded in each button's value field\n// as a stringified JSON snippet containing just the requestId.\n// Full context lives in the DB \u2014 buttons carry only the key.\n// ============================================================\n\nconst item = $json;\n\nconst requestId  = item.requestId;\nconst subject    = item.subject;\nconst submittedBy = item.submittedBy;\nconst details    = item.details;\nconst amount     = item.amount ? `*Amount:* ${item.amount}` : '';\nconst requestType = item.requestType;\nconst timestamp  = new Date().toISOString();\n\n// Button value carries only requestId \u2014 DB holds full context\nconst buttonValue = JSON.stringify({ requestId });\n\nif (buttonValue.length > 1900) {\n  throw new Error(`Button value exceeds Slack 2000 char limit: ${buttonValue.length} chars`);\n}\n\nconst blocks = [\n  {\n    type: 'header',\n    text: { type: 'plain_text', text: `\ud83d\udccb Approval Request \u2014 ${requestType}`, emoji: true }\n  },\n  {\n    type: 'section',\n    text: { type: 'mrkdwn', text: `*${subject}*\\n${details}` }\n  },\n  {\n    type: 'section',\n    fields: [\n      { type: 'mrkdwn', text: `*Submitted by:*\\n${submittedBy}` },\n      { type: 'mrkdwn', text: `*Request ID:*\\n\\`${requestId}\\`` },\n      ...(amount ? [{ type: 'mrkdwn', text: amount }] : []),\n      { type: 'mrkdwn', text: `*Submitted at:*\\n${timestamp}` }\n    ]\n  },\n  { type: 'divider' },\n  {\n    type: 'actions',\n    elements: [\n      {\n        type: 'button',\n        text: { type: 'plain_text', text: '\u2705 Approve', emoji: true },\n        style: 'primary',\n        action_id: 'approve',\n        value: buttonValue\n      },\n      {\n        type: 'button',\n        text: { type: 'plain_text', text: '\u274c Reject', emoji: true },\n        style: 'danger',\n        action_id: 'reject',\n        value: buttonValue\n      },\n      {\n        type: 'button',\n        text: { type: 'plain_text', text: '\ud83d\udea9 Flag for Review', emoji: true },\n        action_id: 'flag',\n        value: buttonValue\n      }\n    ]\n  },\n  {\n    type: 'context',\n    elements: [\n      { type: 'mrkdwn', text: `_AutomiQ HITL Router \u2014 Request ID: ${requestId}_` }\n    ]\n  }\n];\n\nreturn [{\n  json: {\n    ...item,\n    blocks,\n    timestamp\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "cbe22789-cffd-4e05-a272-f4d662bbd6c0",
      "name": "Send Message to Slack Channel",
      "type": "n8n-nodes-base.slack",
      "notes": "Native Slack node using Bot Token credential. Posts Block Kit message and returns ts + channel in response for DB storage. Credential needs: chat:write scope.",
      "position": [
        3664,
        1488
      ],
      "parameters": {
        "text": "={{ `Approval Request: ${$json.subject}` }}",
        "select": "channel",
        "blocksUi": "={{ JSON.stringify($json.blocks) }}",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.channel ?? $env.SLACK_CHANNEL_ID }}"
        },
        "messageType": "block",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "609c6568-d4d4-429c-a1ff-343bcdd50b6f",
      "name": "Check Slack Response Validity",
      "type": "n8n-nodes-base.code",
      "notes": "Extracts ts and channel from the native Slack node response. ts is required for chat.update in the callback path to remove buttons post-decision.",
      "position": [
        3888,
        1488
      ],
      "parameters": {
        "jsCode": "// ============================================================\n// EXTRACT ts FROM NATIVE SLACK NODE RESPONSE\n// The native Slack node (message.post) returns the full API\n// response. ts is available at $json.ts and channel at\n// $json.channel \u2014 same structure as the raw API.\n// ============================================================\n\nconst slackResponse = $json;\nconst intake = $('Create Slack Block Message').first().json;\n\n// Native node throws on API error, but double-check ts exists\nconst messageTs = slackResponse.ts ?? slackResponse.message?.ts;\nconst channelId = slackResponse.channel;\n\nif (!messageTs) {\n  throw new Error('Slack response missing ts \u2014 cannot store for later chat.update. Check Bot Token has chat:write scope.');\n}\nif (!channelId) {\n  throw new Error('Slack response missing channel \u2014 cannot store for later chat.update.');\n}\n\nreturn [{\n  json: {\n    ...intake,\n    messageTs,\n    channelId,\n    slackOk: true\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "2a750944-c85a-4e89-b0f4-8d345cd71891",
      "name": "Log Request in Database",
      "type": "n8n-nodes-base.postgres",
      "position": [
        4240,
        1488
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "approval_requests"
        },
        "schema": {
          "__rl": true,
          "mode": "name",
          "value": "public"
        },
        "columns": {
          "value": {
            "status": "pending",
            "context": "={{ JSON.stringify({ requestType: $json.requestType, submittedBy: $json.submittedBy, subject: $json.subject, details: $json.details, amount: $json.amount, metadata: $json.metadata }) }}",
            "channel_id": "={{ $json.channelId }}",
            "message_ts": "={{ $json.messageTs }}",
            "request_id": "={{ $json.requestId }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {}
      },
      "typeVersion": 2.5
    },
    {
      "id": "db079932-ed06-4526-a149-9b21fe1d7adf",
      "name": "Acknowledge Intake with 200 OK",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        4496,
        1488
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        },
        "respondWith": "json",
        "responseBody": "={\"status\":\"ok\",\"requestId\":\"{{ $json.requestId }}\"}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "0746e567-ae83-48ff-85ea-3d07349d3ce1",
      "name": "Handle Slack Callback Data",
      "type": "n8n-nodes-base.code",
      "position": [
        3488,
        1888
      ],
      "parameters": {
        "jsCode": "// ============================================================\n// PARSE SLACK CALLBACK PAYLOAD\n// Extracts the action, user, channel, message ts, and the\n// requestId from the button value. All fields needed for:\n// 1. Immediate ACK (200 OK to Slack within 3 seconds)\n// 2. chat.update to remove buttons from original message\n// 3. DB lookup to retrieve full approval context\n// ============================================================\n\nconst slackPayload = $json.slackPayload;\n\nif (!slackPayload || !slackPayload.actions || slackPayload.actions.length === 0) {\n  throw new Error('Invalid Slack callback payload \u2014 no actions found');\n}\n\nconst action     = slackPayload.actions[0];\nconst actionId   = action.action_id;   // 'approve' | 'reject' | 'flag'\nconst buttonValue = JSON.parse(action.value ?? '{}');\nconst requestId  = buttonValue.requestId;\n\nconst userId     = slackPayload.user?.id ?? 'unknown';\nconst userName   = slackPayload.user?.name ?? 'unknown';\nconst channelId  = slackPayload.channel?.id ?? slackPayload.container?.channel_id;\nconst messageTs  = slackPayload.container?.message_ts ?? slackPayload.message?.ts;\nconst responseUrl = slackPayload.response_url;\n\nif (!requestId) throw new Error('Button value missing requestId \u2014 cannot look up context');\nif (!channelId) throw new Error('Cannot determine channel from Slack payload');\nif (!messageTs) throw new Error('Cannot determine message ts from Slack payload');\n\nconst decidedAt = new Date().toISOString();\n\nreturn [{\n  json: {\n    actionId,\n    requestId,\n    userId,\n    userName,\n    channelId,\n    messageTs,\n    responseUrl,\n    decidedAt\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "0853ad96-7819-4c64-bfab-19a99b360bf8",
      "name": "Acknowledge Slack Callback Fast",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        3680,
        1888
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        },
        "respondWith": "text"
      },
      "typeVersion": 1.1
    },
    {
      "id": "3ef17811-2d21-4874-87ed-e54aab06b952",
      "name": "Retrieve Context from Database",
      "type": "n8n-nodes-base.postgres",
      "position": [
        3872,
        1888
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "approval_requests"
        },
        "where": {
          "values": [
            {
              "value": "={{ $('Handle Slack Callback Data').first().json.requestId }}",
              "column": "request_id"
            }
          ]
        },
        "schema": {
          "__rl": true,
          "mode": "name",
          "value": "public"
        },
        "options": {
          "queryBatching": "independently"
        },
        "operation": "select"
      },
      "typeVersion": 2.5
    },
    {
      "id": "074b5d6c-839f-4838-9478-016f050fd3ab",
      "name": "Combine Callback with DB Context",
      "type": "n8n-nodes-base.code",
      "position": [
        4144,
        1888
      ],
      "parameters": {
        "jsCode": "// ============================================================\n// MERGE CALLBACK + DB CONTEXT\n// Combines the parsed Slack callback fields with the full\n// context loaded from the database into a single clean item\n// for downstream nodes.\n// ============================================================\n\nconst callback = $('Handle Slack Callback Data').first().json;\nconst dbRow    = $input.first().json;\n\nif (!dbRow || !dbRow.request_id) {\n  throw new Error(`No DB record found for requestId: ${callback.requestId}`);\n}\n\nif (dbRow.status !== 'pending') {\n  throw new Error(`Request ${callback.requestId} already decided: ${dbRow.status}. Ignoring duplicate callback.`);\n}\n\nlet context = {};\ntry {\n  context = typeof dbRow.context === 'string' ? JSON.parse(dbRow.context) : dbRow.context;\n} catch(e) {\n  context = { raw: dbRow.context };\n}\n\nreturn [{\n  json: {\n    ...callback,\n    ...context,\n    requestId:  dbRow.request_id,\n    channelId:  callback.channelId  ?? dbRow.channel_id,\n    messageTs:  callback.messageTs  ?? dbRow.message_ts,\n    createdAt:  dbRow.created_at\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "84d90ecc-010d-460a-afb3-cb280c3b6ffc",
      "name": "Update Slack Message Content",
      "type": "n8n-nodes-base.code",
      "position": [
        4368,
        1888
      ],
      "parameters": {
        "jsCode": "// ============================================================\n// BUILD UPDATED SLACK MESSAGE\n// Replaces the interactive buttons with a static status line\n// showing who made the decision and when. Prevents zombie\n// buttons that remain clickable after a decision is made.\n// ============================================================\n\nconst item = $json;\n\nconst actionEmoji = {\n  approve: '\u2705',\n  reject:  '\u274c',\n  flag:    '\ud83d\udea9'\n};\nconst actionLabel = {\n  approve: 'Approved',\n  reject:  'Rejected',\n  flag:    'Flagged for Review'\n};\n\nconst emoji = actionEmoji[item.actionId] ?? '\u26aa';\nconst label = actionLabel[item.actionId] ?? item.actionId;\n\nconst updatedBlocks = [\n  {\n    type: 'header',\n    text: { type: 'plain_text', text: `\ud83d\udccb Approval Request \u2014 ${item.requestType ?? 'General'}`, emoji: true }\n  },\n  {\n    type: 'section',\n    text: { type: 'mrkdwn', text: `*${item.subject ?? 'Approval Request'}*\\n${item.details ?? ''}` }\n  },\n  { type: 'divider' },\n  {\n    type: 'section',\n    text: {\n      type: 'mrkdwn',\n      text: `${emoji} *${label}* by <@${item.userId}> at ${item.decidedAt}`\n    }\n  },\n  {\n    type: 'context',\n    elements: [\n      { type: 'mrkdwn', text: `_Request ID: ${item.requestId} \u2014 AutomiQ HITL Router_` }\n    ]\n  }\n];\n\nreturn [{\n  json: {\n    ...item,\n    updatedBlocks\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "f7a5f5bc-ed33-460d-945b-4a9a06b3b4e2",
      "name": "Modify Slack Message to Final",
      "type": "n8n-nodes-base.slack",
      "notes": "Native Slack node updating the original message using the ts captured during postMessage. Replaces interactive buttons with a static status line. Requires chat:write scope on the Bot Token credential.",
      "position": [
        4592,
        1888
      ],
      "parameters": {
        "ts": "={{ $json.messageTs }}",
        "text": "={{ `Decision recorded: ${$json.actionId}` }}",
        "blocksUi": "={{ JSON.stringify($json.updatedBlocks) }}",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.channelId }}"
        },
        "operation": "update",
        "messageType": "block",
        "otherOptions": {},
        "updateFields": {}
      },
      "typeVersion": 2.2
    },
    {
      "id": "78b3d331-21e5-4cf9-9c8b-e914ceda4dae",
      "name": "Record Decision in Database",
      "type": "n8n-nodes-base.postgres",
      "position": [
        4864,
        1888
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "approval_requests"
        },
        "schema": {
          "__rl": true,
          "mode": "name",
          "value": "public"
        },
        "columns": {
          "value": {
            "status": "={{ $json.actionId === 'approve' ? 'approved' : $json.actionId === 'reject' ? 'rejected' : 'flagged' }}",
            "decided_at": "={{ $json.decidedAt }}",
            "decided_by": "={{ $json.userId }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {},
        "operation": "update"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "2d354365-199b-4988-a473-f1c76d21ff10",
      "name": "Determine Next Action Taken",
      "type": "n8n-nodes-base.if",
      "position": [
        5088,
        1888
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-action",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.actionId }}",
              "rightValue": "approve"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "0dc0536a-5d0f-4a6c-bfcc-5d80d950a49c",
      "name": "Verify Slack Request Signature",
      "type": "n8n-nodes-base.code",
      "position": [
        2768,
        1616
      ],
      "parameters": {
        "jsCode": "// SLACK SIGNATURE VALIDATION\nconst crypto = require('crypto');\nconst headers = $json.headers ?? {};\nconst body = $json.body ?? $json;\nconst rawBody = $json.rawBody ?? JSON.stringify(body);\nconst slackSignature = headers['x-slack-signature'];\nconst slackTimestamp = headers['x-slack-request-timestamp'];\nconst signingSecret = $env.SLACK_SIGNING_SECRET;\nconst isSlackCallback = !!(slackSignature && slackTimestamp);\nif (isSlackCallback) {\n  if (!signingSecret) throw new Error('SLACK_SIGNING_SECRET env var not set');\n  const now = Math.floor(Date.now() / 1000);\n  const requestAge = now - parseInt(slackTimestamp, 10);\n  if (requestAge > 300) throw new Error(`Slack request timestamp too old: ${requestAge}s`);\n  const sigBasestring = `v0:${slackTimestamp}:${rawBody}`;\n  const computedSig = 'v0=' + crypto.createHmac('sha256', signingSecret).update(sigBasestring, 'utf8').digest('hex');\n  const sigBuffer = Buffer.from(slackSignature, 'utf8');\n  const computedBuffer = Buffer.from(computedSig, 'utf8');\n  if (sigBuffer.length !== computedBuffer.length || !crypto.timingSafeEqual(sigBuffer, computedBuffer)) throw new Error('Slack signature mismatch \u2014 request rejected');\n}\nreturn [{ json: { ...$json, signatureValidated: isSlackCallback } }];"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "865b3506-fef3-4763-bd67-500f5c438f7f",
  "nodeGroups": [],
  "connections": {
    "Route by Request Type": {
      "main": [
        [
          {
            "node": "Create Slack Block Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Handle Slack Callback Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Request in Database": {
      "main": [
        [
          {
            "node": "Acknowledge Intake with 200 OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Slack Block Message": {
      "main": [
        [
          {
            "node": "Send Message to Slack Channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Handle Slack Callback Data": {
      "main": [
        [
          {
            "node": "Acknowledge Slack Callback Fast",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Receive Slack POST Request": {
      "main": [
        [
          {
            "node": "Verify Slack Request Signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Record Decision in Database": {
      "main": [
        [
          {
            "node": "Determine Next Action Taken",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Determine Slack Request Type": {
      "main": [
        [
          {
            "node": "Route by Request Type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Slack Message Content": {
      "main": [
        [
          {
            "node": "Modify Slack Message to Final",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Slack Response Validity": {
      "main": [
        [
          {
            "node": "Log Request in Database",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Modify Slack Message to Final": {
      "main": [
        [
          {
            "node": "Record Decision in Database",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Message to Slack Channel": {
      "main": [
        [
          {
            "node": "Check Slack Response Validity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retrieve Context from Database": {
      "main": [
        [
          {
            "node": "Combine Callback with DB Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Slack Request Signature": {
      "main": [
        [
          {
            "node": "Determine Slack Request Type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Acknowledge Slack Callback Fast": {
      "main": [
        [
          {
            "node": "Retrieve Context from Database",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Callback with DB Context": {
      "main": [
        [
          {
            "node": "Update Slack Message Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}