{
  "id": "MOiVECfgXV4dPT9j",
  "name": "BlueSky Suite: Auto-DM followers who reply with a keyword",
  "tags": [],
  "nodes": [
    {
      "id": "48a8cfd9-ba66-45e0-b03d-5594b6116389",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1200,
        -16
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 30
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "21e31f2e-ee46-4612-b07f-3ed66423b48d",
      "name": "BlueSky Auth",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -400,
        -16
      ],
      "parameters": {
        "url": "https://bsky.social/xrpc/com.atproto.server.createSession",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"identifier\":\"{{$('Configuration').first().json.bluesky_handle}}\",\n  \"password\": \"{{ $('Configuration').first().json.app_password }}\"\n}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.3
    },
    {
      "id": "dd9d79a5-242a-434b-8fdc-fcc3d8bd40bb",
      "name": "Get Notifications",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -112,
        -16
      ],
      "parameters": {
        "url": "https://bsky.social/xrpc/app.bsky.notification.listNotifications",
        "options": {},
        "sendQuery": true,
        "sendHeaders": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "limit",
              "value": "25"
            },
            {
              "name": "reason",
              "value": "reply"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $('BlueSky Auth').first().json.accessJwt }}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "a58b587f-33c2-455c-9ab4-8470ec446cc1",
      "name": "Split Out",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        160,
        -16
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "notifications"
      },
      "typeVersion": 1
    },
    {
      "id": "f033d8a7-5ddd-4323-a333-4ead2d740d3f",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1056,
        -16
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "7cb3dac5-8c5e-42f4-885a-fc6d743055ed",
      "name": "Does Follow?",
      "type": "n8n-nodes-base.if",
      "position": [
        1792,
        0
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "ddbb45ea-d926-4ce7-a30b-dcef7320c218",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.viewer.followedBy }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "caa05d8b-c155-493a-8270-76b1465ec876",
      "name": "Get Convo ID",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2144,
        -16
      ],
      "parameters": {
        "url": "https://api.bsky.chat/xrpc/chat.bsky.convo.getConvoForMembers",
        "options": {},
        "sendQuery": true,
        "sendHeaders": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "members",
              "value": "={{ $('Check Follow Status').item.json.did }}"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $('BlueSky Auth').first().json.accessJwt }}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "d1be382a-b803-47db-99f7-b0ed47b89e40",
      "name": "Check Follow Status",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1456,
        0
      ],
      "parameters": {
        "url": "https://bsky.social/xrpc/app.bsky.actor.getProfile",
        "options": {},
        "sendQuery": true,
        "sendHeaders": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "actor",
              "value": "={{ $json.author.did }}"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $('BlueSky Auth').first().json.accessJwt }}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "92195ea1-52c7-4d85-8806-826092569ab3",
      "name": "Send DM",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2464,
        -16
      ],
      "parameters": {
        "url": "https://api.bsky.chat/xrpc/chat.bsky.convo.sendMessage",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"convoId\": \"{{ $json.convo.id }}\",\n  \"message\" : {\n                \"text\" : \"{{ $('Configuration').first().json.dm_message }}\"\n              }\n} ",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $('BlueSky Auth').first().json.accessJwt }}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "6eee413e-ac79-450f-b949-4358bcf579f6",
      "name": "Filter New Only",
      "type": "n8n-nodes-base.code",
      "position": [
        704,
        -16
      ],
      "parameters": {
        "jsCode": "const staticData = $getWorkflowStaticData('global');\nconst lastProcessed = staticData.lastProcessedTime || '1970-01-01T00:00:00.000Z';\nconst newItems = [];\nlet maxDate = lastProcessed;\n\nfor (const item of $input.all()) {\n  // indexedAt is the safest timestamp to use for notifications\n  const notifTime = item.json.indexedAt; \n  if (notifTime > lastProcessed) {\n    newItems.push(item);\n    if (notifTime > maxDate) {\n      maxDate = notifTime;\n    }\n  }\n}\n\n// Update memory immediately so we don't process these again\nstaticData.lastProcessedTime = maxDate;\n\nreturn newItems;"
      },
      "typeVersion": 2
    },
    {
      "id": "f8ec376e-fd9e-4427-9f5a-028e9a920c67",
      "name": "Filter Reply contains",
      "type": "n8n-nodes-base.filter",
      "position": [
        432,
        -16
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "4278f478-365c-4e41-93cd-abc0b23098dd",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.reason }}",
              "rightValue": "reply"
            },
            {
              "id": "6d0c5b01-bc69-4be3-91eb-b0f8bdc55944",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json.record.text }}",
              "rightValue": "={{ $('Configuration').first().json.trigger_keyword }}"
            },
            {
              "id": "12c06f9f-ff8a-4656-ab6a-ae731ce0acdc",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json.record.reply.root.uri }}",
              "rightValue": "={{ $('Extract Post ID').first().json.post_id }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "0af999ea-fbed-4212-95c5-b52e0eb829b5",
      "name": "Like the reply",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2720,
        -16
      ],
      "parameters": {
        "url": "https://bsky.social/xrpc/com.atproto.repo.createRecord",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"repo\": \"{{ $('BlueSky Auth').first().json.did }}\",\n  \"collection\": \"app.bsky.feed.like\",\n  \"record\": {\n    \"$type\": \"app.bsky.feed.like\",\n    \"createdAt\": \"{{ new Date().toISOString() }}\",\n    \"subject\": {\n      \"uri\": \"{{ $('Loop Over Items').item.json.uri }}\",\n      \"cid\": \"{{ $('Loop Over Items').item.json.cid }}\"\n    }\n  }\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $('BlueSky Auth').first().json.accessJwt }}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "71c94cb7-a064-4256-8a05-4bb1459f6e66",
      "name": "Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        -960,
        -16
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "4e2b2be1-d052-4dc8-96e8-60054b1cba1d",
              "name": "bluesky_handle",
              "type": "string",
              "value": ""
            },
            {
              "id": "b336da17-f730-49e6-85c9-4f8e9b3f8522",
              "name": "app_password",
              "type": "string",
              "value": ""
            },
            {
              "id": "b1e4781c-a8e6-444d-91cd-b280d6ba7da4",
              "name": "target_post_url",
              "type": "string",
              "value": ""
            },
            {
              "id": "efde8a81-fbdc-40d7-836f-916387741c5f",
              "name": "trigger_keyword",
              "type": "string",
              "value": ""
            },
            {
              "id": "5783aa54-8af1-4330-abce-48045ab2a710",
              "name": "dm_message",
              "type": "string",
              "value": ""
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "52037176-213b-405d-9ffd-dd815c0ef2f5",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1056,
        -304
      ],
      "parameters": {
        "width": 304,
        "height": 272,
        "content": "### 1- START HERE \nEnter your \n- BlueSky Handle (e.g., steve.bsky.social)\n- App Password. \nThis sets up the authentication for the whole workflow.\n- Trigger Keyword: The word users must type (e.g., Linkedin).\n- DM text: Message to send followers when they reply with the \"Trigger Keyword\""
      },
      "typeVersion": 1
    },
    {
      "id": "895a8910-64d0-418a-994d-e013b9a41896",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -464,
        144
      ],
      "parameters": {
        "width": 272,
        "height": 176,
        "content": "### 2- Get access token\nLogin to BlueSky using handle and app password to get the access token. This will be used in all future BlueSky api calls."
      },
      "typeVersion": 1
    },
    {
      "id": "c6819087-4d54-4e97-98d5-e0726ce04bac",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -176,
        -208
      ],
      "parameters": {
        "height": 176,
        "content": "### 3- Google sheets rows \nFetcher Grabs all rows where Status column is \"Posted\". \n\nIf you want to test, manually add a row with \"Post\" in your sheet first."
      },
      "typeVersion": 1
    },
    {
      "id": "cf33b0be-0758-41cd-9660-c4e4eaa01daf",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2192,
        -448
      ],
      "parameters": {
        "width": 768,
        "height": 832,
        "content": "\n# \ud83d\ude80 Lead Magnet / Auto-DM Bot - How To Use\n\n**Goal:** Automatically DM a link/resource to anyone who replies to your posts with a specific keyword (e.g., \"LinkedIn\").\n\n**Step 1:** Campaign Setup Open the \"Configuration\" node (first green node) and set:\n\n- Handle & Password: Your BlueSky credentials.\n- Trigger Keyword: The word users must type (e.g., Template).\n- DM Message: The message they will receive (e.g., \"Hey! Here is the link: ...\").\n\n\n**Step 2:** The \"Growth Hack\" Logic This workflow checks 3 things before sending a message:\n\n- Did they use the Keyword?\n- Is this a New reply? (Prevents duplicate DMs).\n- Do they Follow you? (Incentivizes followers).\n\n\n**Step 3:** Activate Turn it to \"Active\". It runs every 15 minutes, scanning your notifications for new leads.\n\n[**Sample Google Sheet**](https://docs.google.com/spreadsheets/d/1Mg04gK1K5DBtJHrWw3ePRFc_JjkxwAp0deGjapVl2q0/edit?usp=sharing)\n\n\n\n## \u26a0\ufe0f READ BEFORE TESTING\n\n**In Manual Testing Mode:**\nn8n wipes the memory (`staticData`) every time you click \"Execute\". The bot will **forget** previous runs and may send duplicate DMs to the same user during manual tests.\n\n**To Test Deduplication Properly:**\n1. **Activate** the workflow (Toggle \"Active\" in top right).\n2. Reply to your post on BlueSky using the keyword.\n3. Wait for the workflow to run automatically (every 15 min).\n4. Check \"Executions\" to confirm it only processed the reply **once**."
      },
      "typeVersion": 1
    },
    {
      "id": "8e6c2979-f0f8-4ea6-861a-1be999d58a48",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        336,
        -224
      ],
      "parameters": {
        "width": 288,
        "height": 176,
        "content": "### 5- The Keyword Filter \nFilters the stream to only allow:\n- Replies (ignores likes/reposts).\n- Text that contains your Trigger keyword (from the Config node)."
      },
      "typeVersion": 1
    },
    {
      "id": "43c4717d-936c-4566-802b-a2456ad99572",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        592,
        128
      ],
      "parameters": {
        "width": 288,
        "height": 224,
        "content": "### 6- The Memory Gatekeeper \n**\u26a0\ufe0f System Node: Handles deduplication. No configuration needed here.**\n\nThis script remembers the timestamp of the last processed reply. It ensures that even if the workflow runs 100 times, it never sends a duplicate DM to the same user for the same reply."
      },
      "typeVersion": 1
    },
    {
      "id": "b1eb13d4-e41f-43b5-b15f-e55e3332de63",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        80,
        128
      ],
      "parameters": {
        "width": 272,
        "height": 176,
        "content": "### 4- The Unpacker \nThe BlueSky API sends us one big list containing 25 notifications. This node \"explodes\" that list into 25 separate items so we can filter and process each notification individually."
      },
      "typeVersion": 1
    },
    {
      "id": "5c2de170-2e51-42e0-b882-483bcd6939a3",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1328,
        -240
      ],
      "parameters": {
        "width": 320,
        "height": 192,
        "content": "### 8- The Growth Hook \nWe check if the user follows you.\n\nIf YES: They get the DM.\n\nIf NO: We skip them. (This encourages users to follow you to get the resource)."
      },
      "typeVersion": 1
    },
    {
      "id": "91e64058-1aee-4ffe-ac5a-f406a4029e90",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2656,
        -256
      ],
      "parameters": {
        "width": 272,
        "height": 224,
        "content": "### 12- Like the reply\nThe \"Delight\" Factor Before finishing, we \"Like\" the user's reply. \n\nThis gives them a notification \"Creator liked your reply\", confirming you saw them, even if the DM takes a moment to arrive."
      },
      "typeVersion": 1
    },
    {
      "id": "00376854-1cf0-4093-92e0-6a21a48817f2",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        992,
        -240
      ],
      "parameters": {
        "width": 304,
        "height": 192,
        "content": "### 7- The Processor \nTakes the \"Winning Replies\" (those that passed the keyword and duplicate checks) and handles them one by one. \n\nThis ensures that the variables for User A don't get mixed up with User B."
      },
      "typeVersion": 1
    },
    {
      "id": "ddda05e4-3a9c-4035-8baf-71cec5e0bf9f",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1680,
        -256
      ],
      "parameters": {
        "width": 320,
        "height": 224,
        "content": "### 9- The Growth Gate \nThis checks the data from the previous node (\"Check Follow Status\").\n\nTrue Path: The user follows you \u2192 Send them the DM.\n\nFalse Path: They don't follow \u2192 Stop. (This effectively \"locks\" your lead magnet behind a follow)."
      },
      "typeVersion": 1
    },
    {
      "id": "18bb1fc1-a24c-43a6-9f67-b34a4d63bf56",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2032,
        -240
      ],
      "parameters": {
        "width": 288,
        "height": 208,
        "content": "### 10- The Handshake \nYou cannot send a DM directly to a User ID. \n\nYou must first ask the Chat API to find (or create) a Conversation ID between you and that user. \n\nThis node performs that \"handshake.\""
      },
      "typeVersion": 1
    },
    {
      "id": "74866337-43d3-48aa-a059-8989028c5f3c",
      "name": "Sticky Note12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2352,
        -256
      ],
      "parameters": {
        "width": 272,
        "height": 224,
        "content": "### 11- The Delivery \nUses the Conversation ID we just retrieved to send the actual text message. \n\n**Note:** The message text is pulled dynamically from your Configuration node at the start."
      },
      "typeVersion": 1
    },
    {
      "id": "4567c40a-992a-4a4f-930f-512faf9f3f1c",
      "name": "Sticky Note13",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -496,
        -352
      ],
      "parameters": {
        "color": 7,
        "width": 1408,
        "height": 752,
        "content": "# Input Processing"
      },
      "typeVersion": 1
    },
    {
      "id": "538d0981-6894-4840-aa25-e125e0fe2f19",
      "name": "Sticky Note14",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        960,
        -352
      ],
      "parameters": {
        "color": 7,
        "width": 2016,
        "height": 752,
        "content": "# Fulfillment Logic"
      },
      "typeVersion": 1
    },
    {
      "id": "9905acda-04cd-48dc-ab43-48c6ce6af70b",
      "name": "Extract Post ID",
      "type": "n8n-nodes-base.set",
      "position": [
        -752,
        -16
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cbbc7ebb-715d-4156-b52a-5988f1c559f3",
              "name": "post_id",
              "type": "string",
              "value": "={{ $('Configuration').first().json.target_post_url.match(/post\\/([a-zA-Z0-9]+)/)[1] }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "d9d0364a-4b94-427d-bc5c-aca82bae8100",
  "connections": {
    "Send DM": {
      "main": [
        [
          {
            "node": "Like the reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Out": {
      "main": [
        [
          {
            "node": "Filter Reply contains",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "BlueSky Auth": {
      "main": [
        [
          {
            "node": "Get Notifications",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Does Follow?": {
      "main": [
        [
          {
            "node": "Get Convo ID",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Convo ID": {
      "main": [
        [
          {
            "node": "Send DM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configuration": {
      "main": [
        [
          {
            "node": "Extract Post ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Like the reply": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Post ID": {
      "main": [
        [
          {
            "node": "BlueSky Auth",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter New Only": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [],
        [
          {
            "node": "Check Follow Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Notifications": {
      "main": [
        [
          {
            "node": "Split Out",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Follow Status": {
      "main": [
        [
          {
            "node": "Does Follow?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Reply contains": {
      "main": [
        [
          {
            "node": "Filter New Only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}