{
  "name": "Qualify target accounts from their job postings and draft the outreach opener with Apify, OpenAI and Google Sheets",
  "nodes": [
    {
      "parameters": {
        "content": "## Qualify your target accounts from who they are hiring, and draft the opener\n\nA job posting is the one buying signal a company publishes about itself on purpose. A new RevOps role means they are about to buy RevOps tooling. Three data engineers in a month means a warehouse project. Nobody announces that in a press release, and it is sitting in public two weeks before your competitor notices.\n\nThe hard part is not finding the postings, it is reading 40 of them and deciding which five accounts are worth contacting this week, then writing something that does not sound like a template. This workflow does both.\n\n### Who it's for\nFounders doing their own outbound, SDRs working a named account list, and agencies running outbound for clients. If you keep your accounts in a Google Sheet, this reads that sheet.\n\n### How it works\n1. **Schedule** runs it every Monday morning.\n2. **Set your targets** holds what you sell, the hiring patterns you care about, and how strict to be.\n3. Your **account list** is read straight from your Google Sheet, one domain per row.\n4. **Apify** runs the [Company Domain to Job Postings](https://apify.com/fayoussef/company-domain-to-job-postings?fpr=youssef) Actor, which resolves each domain to its real job board (Greenhouse, Lever, Workday, Ashby, Recruitee and others) and returns the open roles with their descriptions.\n5. **Remove Duplicates** drops postings you were already shown, so a weekly run costs no AI tokens on roles it already read.\n6. Postings are **grouped per account**, because the account is the unit of work, not the posting.\n7. **OpenAI reads each account's hiring** and scores 0 to 100 on whether it signals a need for what you sell, names the signal in one phrase, and quotes the evidence. Every account is written to Google Sheets, including the ones it rejects, so you can see what was filtered and tune `minScore`.\n8. **OpenAI writes the opener** for the accounts that pass: a subject line and 80 words built on the hiring observation, not on flattery.\n9. Each one arrives in Telegram as its own message, ready to copy into your sequence.\n\n### How to set up\n1. Create a free [Apify account](https://apify.com/?fpr=youssef) and connect it in the **Apify** node.\n2. Connect an OpenAI account in the **OpenAI model** node.\n3. Put your account list in a Google Sheet: one tab with a column of company domains (`stripe.com`, not a LinkedIn URL). Paste the sheet URL into **Set your targets** and set `domainColumn` to that column's header.\n4. Add a second, empty tab for the briefs and put its name in `briefTab`. The first run writes the headers itself.\n5. Create a Telegram bot with @BotFather, connect it, and paste your chat id into **Set your targets**. To find your chat id, message @userinfobot and it replies with it.\n6. Fill in `whatYouSell`, `problemYouSolve` and `signalsYouCareAbout`. The scoring is only as good as these three lines, so be specific about the problem, not the product.\n\n### Requirements\n- Apify account (the free plan covers a short account list per run)\n- An OpenAI account, Google Sheets and a Telegram bot\n\n### How to customize\n- Raise `minScore` if too many weak accounts get through, lower it if a week goes quiet. 60 to 70 is the useful band.\n- `titleKeywords` narrows what counts as relevant hiring before any AI reads it, which is the cheapest filter you have.\n- Edit the system prompt in **Write the outreach opener** for your own voice, or swap the **OpenAI model** node for Anthropic or Gemini.\n- **Read every opener before you send it.** The prompt forbids inventing facts about the account, but no prompt makes that guaranteed, and this one goes out under your name.",
        "height": 1740,
        "width": 520,
        "color": 1
      },
      "id": "hs22b701-0000-4000-8000-000000000001",
      "name": "Sticky Note - About",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -760,
        -240
      ]
    },
    {
      "parameters": {
        "content": "### 1. What you sell, and what counts as a signal\nEverything downstream is built from this node, so it is worth ten minutes.\n\n`whatYouSell`: one plain line. No tagline.\n\n`problemYouSolve`: the operational pain. \"Their CRM data is wrong because nobody owns it\" scores accounts far better than \"revenue intelligence platform\", because the model is matching postings against a problem, not a category.\n\n`signalsYouCareAbout`: the hiring patterns that mean someone is about to buy. Comma separated, concrete: \"first RevOps hire, data engineer after a Series A, anyone owning CRM hygiene\".\n\n`titleKeywords`: which roles to fetch at all. Leave it empty to read everything they have open.\n\n`minScore`: 0 to 100. Start at 65. The scorer is told that growth alone is not a signal, so most accounts land below 40 on any given week, and that is correct behaviour rather than a broken run.\n\n`maxAccounts`: the most openers to write per run.",
        "height": 760,
        "width": 520,
        "color": 7
      },
      "id": "hs22b701-0000-4000-8000-000000000002",
      "name": "Sticky Note - Step 1",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -200,
        -240
      ]
    },
    {
      "parameters": {
        "content": "### 2. Your own list, then their real job board\nThe account list is read from your Google Sheet, so this workflow fits the list you already keep. One row per company, one column of domains, named by `domainColumn`.\n\nDomains are cleaned before they are sent, so `https://www.Stripe.com/careers` and `stripe.com` are the same account, and repeats are collapsed.\n\nThe Actor does the part n8n cannot: it resolves each domain to whichever applicant tracking system that company actually uses and reads the live postings with their full descriptions. There is no single API for this, which is why a list of domains is normally a research afternoon.\n\n`incremental` asks the Actor to return only new or changed postings.",
        "height": 760,
        "width": 840,
        "color": 7
      },
      "id": "hs22b701-0000-4000-8000-000000000003",
      "name": "Sticky Note - Step 2",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        360,
        -240
      ]
    },
    {
      "parameters": {
        "content": "### 3. Read the hiring, not the job titles\n**Remove Duplicates** sits before the AI on purpose: a weekly run that finds nothing new spends nothing on tokens.\n\nThe postings are then grouped per company, because one data engineer is noise and four in a month is a project. The model sees every open role for that account, with up to 700 characters of each description, and decides together.\n\nIt returns a score, the signal in one repeatable phrase, and a quote from the posting it rests on. It is told to quote only words that appear in the postings, and that growth on its own is not a signal.\n\n**Every** account is saved to Google Sheets with its score, including the rejects. Read those for a week and then move `minScore`: that is the tuning loop.",
        "height": 760,
        "width": 880,
        "color": 7
      },
      "id": "hs22b701-0000-4000-8000-000000000004",
      "name": "Sticky Note - Step 3",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1240,
        -240
      ]
    },
    {
      "parameters": {
        "content": "### 4. The brief, including what it rejected\n`Save every account brief` appends one row per account per run to the `briefTab` tab: domain, company, score, the signal, the evidence quote, the roles that were open, and the date.\n\nThat tab is the deliverable even before you read Telegram. It is a worked account list with a reason attached to each line, which is the thing an SDR normally produces by hand on a Monday.\n\nThe first run writes the header row itself, so point `briefTab` at an empty tab and leave it alone.\n\nIf you would rather write the result back into your own account list, change this node's operation to **Append or update** and match on your domain column. The columns have to exist in that tab first, or the node errors.",
        "height": 760,
        "width": 620,
        "color": 7
      },
      "id": "hs22b701-0000-4000-8000-000000000005",
      "name": "Sticky Note - Step 4",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2160,
        -240
      ]
    },
    {
      "parameters": {
        "content": "### 5. Write the opener, not a notification\nOnly accounts at or above `minScore` reach this lane, capped by `maxAccounts`.\n\nThe second chain writes the actual first contact: a subject line and 80 words that open on the hiring observation in the account's own words. It is forbidden to mention headcount, funding, tooling or timelines that are not in the quoted evidence.\n\nEach one arrives as its own Telegram message with the signal and the evidence above it, so you can judge the claim before you copy the text. The link goes to the posting the signal came from.\n\n**Read every opener before you send it.** The prompt is strict about invented facts, but no prompt makes that guaranteed, and the reader of a cold email knows their own company better than the model does.",
        "height": 760,
        "width": 880,
        "color": 7
      },
      "id": "hs22b701-0000-4000-8000-000000000006",
      "name": "Sticky Note - Step 5",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2820,
        -240
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "weeksInterval": 1,
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 8
            }
          ]
        }
      },
      "id": "hs22b701-0000-4000-8000-000000000010",
      "name": "Every Monday at 8am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -120,
        120
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "hs22b701-0000-4000-8000-000000000101",
              "name": "whatYouSell",
              "value": "A RevOps service that cleans up CRM data and keeps it clean",
              "type": "string"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000102",
              "name": "problemYouSolve",
              "value": "Their pipeline reporting is wrong because nobody owns CRM hygiene, so forecasts are guesses and reps do not trust the data.",
              "type": "string"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000103",
              "name": "signalsYouCareAbout",
              "value": "first RevOps or sales ops hire, a data engineer joining a go-to-market team, anyone whose posting mentions CRM cleanup, Salesforce migration or reporting we cannot trust",
              "type": "string"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000104",
              "name": "titleKeywords",
              "value": "revops, revenue operations, sales operations, data engineer, analytics engineer",
              "type": "string"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000105",
              "name": "locations",
              "value": "",
              "type": "string"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000106",
              "name": "domainColumn",
              "value": "Domain",
              "type": "string"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000107",
              "name": "accountTab",
              "value": "Accounts",
              "type": "string"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000108",
              "name": "briefTab",
              "value": "Briefs",
              "type": "string"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000109",
              "name": "maxJobs",
              "value": 100,
              "type": "number"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000110",
              "name": "minScore",
              "value": 65,
              "type": "number"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000111",
              "name": "maxAccounts",
              "value": 5,
              "type": "number"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000112",
              "name": "googleSheetUrl",
              "value": "PASTE_YOUR_GOOGLE_SHEET_URL_HERE",
              "type": "string"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000113",
              "name": "telegramChatId",
              "value": "PASTE_YOUR_TELEGRAM_CHAT_ID_HERE",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "hs22b701-0000-4000-8000-000000000011",
      "name": "Set your targets",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        120,
        120
      ]
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "={{ $('Set your targets').first().json.googleSheetUrl }}",
          "mode": "url"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('Set your targets').first().json.accountTab }}",
          "mode": "name"
        },
        "options": {}
      },
      "id": "hs22b701-0000-4000-8000-000000000012",
      "name": "Read your account list",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        460,
        120
      ]
    },
    {
      "parameters": {
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "accounts",
        "options": {}
      },
      "id": "hs22b701-0000-4000-8000-000000000013",
      "name": "Collect the domains",
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        720,
        120
      ]
    },
    {
      "parameters": {
        "operation": "Run actor and get dataset",
        "actorSource": "store",
        "actorId": {
          "__rl": true,
          "value": "onaa4nFJMg4z70fwu",
          "mode": "id"
        },
        "customBody": "={{ (() => {\n  const s = $('Set your targets').first().json;\n  const list = v => String(v || '').split(',').map(x => x.trim()).filter(Boolean);\n  const col = String(s.domainColumn);\n  const domains = $json.accounts\n    .map(r => String(r[col] ?? '').trim())\n    .filter(Boolean)\n    .map(d => d.toLowerCase().replace(/^https?:\\/\\//, '').replace(/^www\\./, '').replace(/\\/.*$/, ''));\n  return JSON.stringify({\n    companies: [...new Set(domains)],\n    keywords: list(s.titleKeywords),\n    locationFilter: list(s.locations),\n    includeDescription: true,\n    descriptionFormat: 'markdown',\n    incremental: true,\n    maxItems: s.maxJobs\n  });\n})() }}"
      },
      "id": "hs22b701-0000-4000-8000-000000000014",
      "name": "Find who they are hiring",
      "type": "@apify/n8n-nodes-apify.apify",
      "typeVersion": 1,
      "position": [
        980,
        120
      ]
    },
    {
      "parameters": {
        "operation": "removeItemsSeenInPreviousExecutions",
        "dedupeValue": "={{ $json.job_id }}",
        "options": {}
      },
      "id": "hs22b701-0000-4000-8000-000000000015",
      "name": "Skip postings already read",
      "type": "n8n-nodes-base.removeDuplicates",
      "typeVersion": 2,
      "position": [
        1340,
        120
      ]
    },
    {
      "parameters": {
        "jsCode": "const groups = {};\nfor (const item of $input.all()) {\n  const j = item.json;\n  const dom = String(j.source_domain || '').toLowerCase();\n  if (!dom) continue;\n  const g = groups[dom] || (groups[dom] = { domain: dom, company: j.company || dom, roles: [] });\n  g.roles.push(j);\n}\nconst out = [];\nfor (const g of Object.values(groups)) {\n  g.roles.sort((a, b) => String(b.posted_at || '').localeCompare(String(a.posted_at || '')));\n  const top = g.roles.slice(0, 12);\n  const lines = top.map(r => {\n    const bits = [r.title];\n    if (r.department) bits.push('team: ' + r.department);\n    if (r.seniority) bits.push(String(r.seniority).replaceAll('_', ' '));\n    if (r.location) bits.push(r.location);\n    if (r.posted_at) bits.push('posted ' + String(r.posted_at).slice(0, 10));\n    const head = '- ' + bits.join(' | ');\n    const body = String(r.description_markdown || r.description_text || '')\n      .replace(/[#*>`|]/g, ' ').replace(/\\s+/g, ' ').trim().slice(0, 700);\n    return body ? head + '\\n    ' + body : head;\n  });\n  out.push({ json: {\n    domain: g.domain,\n    company: g.company,\n    openRoles: g.roles.length,\n    newRoles: g.roles.filter(r => r.change_status === 'new').length,\n    titles: top.map(r => r.title).join('; '),\n    departments: [...new Set(g.roles.map(r => r.department).filter(Boolean))].join(', '),\n    firstApplyUrl: (top[0] || {}).apply_url || (top[0] || {}).job_url || '',\n    postings: lines.join('\\n')\n  } });\n}\nreturn out;"
      },
      "id": "hs22b701-0000-4000-8000-000000000016",
      "name": "Group postings by account",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1600,
        120
      ]
    },
    {
      "parameters": {
        "text": "=THE SELLER\nWhat they sell: {{ $('Set your targets').first().json.whatYouSell }}\nThe problem it solves: {{ $('Set your targets').first().json.problemYouSolve }}\nHiring patterns worth acting on: {{ $('Set your targets').first().json.signalsYouCareAbout }}\n\nTHE ACCOUNT\nCompany: {{ $json.company }} ({{ $json.domain }})\nOpen roles found: {{ $json.openRoles }} ({{ $json.newRoles }} new since the last run)\nTeams hiring: {{ $json.departments || 'not stated' }}\n\nTHEIR OPEN POSTINGS\n{{ $json.postings }}",
        "messages": {
          "messageValues": [
            {
              "message": "You are a B2B researcher who reads a company's open job postings and decides whether their hiring is a buying signal for one specific seller. You are strict, because a weak signal wastes the seller's one chance to contact that account.\n\nHow to judge the score, 0 to 100: how strongly this hiring suggests the account needs what the seller sells, right now. Score 80 or above only when the postings show the exact problem the seller solves being staffed for, named in the posting text. Score below 40 when the hiring is routine backfill, or unrelated to the seller's offer, or when the only connection is that the company is growing. Growth alone is not a signal.\n\nHow to write the signal: one short phrase naming what the hiring tells you is happening inside that company, for example 'building a first RevOps team', 'standing up a mobile platform team' or 'moving the warehouse to Snowflake'. Do not echo a job title back: 'hiring an Android Engineer' is the input, not a signal. Say what the pattern of roles, their seniority and their wording together imply. When the score is low, say plainly what the hiring is instead, for example 'routine engineering backfill'.\n\nHow to pick the evidence: the words from the postings that prove the signal, up to about 30 of them, quoted exactly as they appear above. Ask whether a stranger reading only your quote would reach your signal. A company's mission statement, its benefits list, its values or any sentence that would appear in every one of its adverts is not evidence, however quotable it sounds. If the proof is the set of roles themselves, quote the role titles. If you cannot quote anything that supports the signal, the score is below 40 and the evidence is an empty string. Never guess at tooling, headcount, funding or plans that the postings do not state.\n\nAnswer with the JSON object the schema asks for and nothing else: no prose before or after it, no code fences, and no labelled lines such as 'Score:'."
            }
          ]
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "id": "hs22b701-0000-4000-8000-000000000017",
      "name": "Read the hiring signal",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.5,
      "position": [
        1860,
        120
      ]
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini"
        },
        "options": {
          "temperature": 0.3
        }
      },
      "id": "hs22b701-0000-4000-8000-000000000018",
      "name": "OpenAI model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.2,
      "position": [
        1600,
        400
      ]
    },
    {
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"score\": { \"type\": \"number\" },\n    \"signal\": { \"type\": \"string\" },\n    \"evidence\": { \"type\": \"string\" }\n  },\n  \"required\": [\"score\", \"signal\", \"evidence\"]\n}"
      },
      "id": "hs22b701-0000-4000-8000-000000000019",
      "name": "Signal format",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.2,
      "position": [
        1860,
        400
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "hs22b701-0000-4000-8000-000000000201",
              "name": "Company",
              "value": "={{ $('Group postings by account').item.json.company }}",
              "type": "string"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000202",
              "name": "Domain",
              "value": "={{ $('Group postings by account').item.json.domain }}",
              "type": "string"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000203",
              "name": "Score",
              "value": "={{ $json.output.score }}",
              "type": "number"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000204",
              "name": "Signal",
              "value": "={{ $json.output.signal }}",
              "type": "string"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000205",
              "name": "Evidence",
              "value": "={{ $json.output.evidence }}",
              "type": "string"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000206",
              "name": "Open roles",
              "value": "={{ $('Group postings by account').item.json.openRoles }}",
              "type": "number"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000207",
              "name": "New roles",
              "value": "={{ $('Group postings by account').item.json.newRoles }}",
              "type": "number"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000208",
              "name": "Roles seen",
              "value": "={{ $('Group postings by account').item.json.titles }}",
              "type": "string"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000209",
              "name": "Example posting",
              "value": "={{ $('Group postings by account').item.json.firstApplyUrl }}",
              "type": "string"
            },
            {
              "id": "hs22b701-0000-4000-8000-000000000210",
              "name": "Checked",
              "value": "={{ $now.format('yyyy-MM-dd') }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "hs22b701-0000-4000-8000-000000000020",
      "name": "Format the account row",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2260,
        120
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Set your targets').first().json.googleSheetUrl }}",
          "mode": "url"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('Set your targets').first().json.briefTab }}",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "id": "hs22b701-0000-4000-8000-000000000021",
      "name": "Save every account brief",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        2520,
        20
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "hs22b701-0000-4000-8000-000000000301",
              "leftValue": "={{ Number($json.Score) >= Number($('Set your targets').first().json.minScore) }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "hs22b701-0000-4000-8000-000000000022",
      "name": "Keep the accounts worth working",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.2,
      "position": [
        2520,
        240
      ]
    },
    {
      "parameters": {
        "maxItems": "={{ $('Set your targets').first().json.maxAccounts }}"
      },
      "id": "hs22b701-0000-4000-8000-000000000023",
      "name": "Cap the accounts per run",
      "type": "n8n-nodes-base.limit",
      "typeVersion": 1,
      "position": [
        2920,
        240
      ]
    },
    {
      "parameters": {
        "text": "=THE SELLER\nWhat they sell: {{ $('Set your targets').first().json.whatYouSell }}\nThe problem it solves: {{ $('Set your targets').first().json.problemYouSolve }}\n\nTHE ACCOUNT\nCompany: {{ $json.Company }} ({{ $json.Domain }})\nRoles they have open: {{ $json['Roles seen'] }}\n\nTHE SIGNAL TO LEAD WITH\n{{ $json.Signal }}\n\nTHE EVIDENCE YOU MAY QUOTE\n{{ $json.Evidence || 'nothing quotable, so quote nothing' }}",
        "messages": {
          "messageValues": [
            {
              "message": "You write the first contact a seller sends to an account, using what that account's job postings reveal. It is cold, so it has to earn the next ten seconds.\n\nReturn a subject line, then a blank line, then the message body. Nothing else.\n\nThe subject line:\n- At most 8 words, sentence case, no colon-separated marketing phrases.\n- It names the problem the reader already has, not your observation about them. 'Android release times' is a subject. 'Hiring an Android engineer' is their own job ad read back at them, and 'quick question' says nothing. Never begin it with the word hiring.\n- Do not reuse the signal phrase as the subject, and do not write the word Subject or any other label in front of it. The signal is your private note about them; the subject is about the problem they live with. Share at most one word with the signal phrase: if the signal is 'building a mobile platform team', then 'building a mobile platform team' and 'mobile platform team' are both wrong, and 'flaky android releases' is right.\n\nThe body:\n- 80 words maximum, plain text, no markdown, no bullet points, no sign-off block.\n- Open on the hiring observation, specifically. Never open with 'I hope this finds you well', 'I noticed you are growing', or praise for the company.\n- Say in one line what the seller does, tied to the problem the hiring implies.\n- Close with one question about how they handle that problem today, not a meeting demand.\n\nWhat you may not write, because the reader knows their own company and will spot it:\n- Do not tell them what their goals, plans, priorities or intentions are. You know which roles they have open, and nothing else about their thinking.\n- Do not write 'in your own words', 'you mentioned', 'as you said', or any phrasing that attributes a quote to them, unless those exact words appear in the evidence below.\n- Do not state their headcount, funding, revenue, tooling, timeline or customers unless it is quoted in the evidence.\n- Inventing any of this is the one unrecoverable mistake here, because the seller may send the message as written.\n\nBanned words and phrases, in any form, because they read as a template: streamline, leverage, enhance, enhancing, optimise, robust, seamless, cutting edge, best practices, synergy, solutions, specialise in, specialize in, just reaching out, quick question, circling back, teams like yours, drive impact. No exclamation marks.\n\nName what the seller does in plain words instead: what it runs, replaces or takes over."
            }
          ]
        },
        "promptType": "define"
      },
      "id": "hs22b701-0000-4000-8000-000000000024",
      "name": "Write the outreach opener",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.5,
      "position": [
        3180,
        240
      ]
    },
    {
      "parameters": {
        "chatId": "={{ $('Set your targets').first().json.telegramChatId }}",
        "text": "={{ (() => {\nconst esc = s => String(s ?? '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');\nconst cut = (s, n) => String(s ?? '').length > n ? String(s).slice(0, n - 1).trimEnd() + '\\u2026' : String(s ?? '');\nconst a = $('Format the account row').item.json;\nconst out = String($json.text || '').trim();\nconst nl = out.indexOf('\\n');\nconst subject = nl === -1 ? '' : out.slice(0, nl).replace(/^subject:\\s*/i, '').trim();\nconst body = nl === -1 ? out : out.slice(nl + 1).trim();\nreturn '\\ud83c\\udfe2 <b>' + esc(a['Company']) + '</b> \\u00b7 ' + esc(a['Domain'])\n  + ' \\u00b7 signal ' + a['Score'] + '/100\\n'\n  + '\\ud83d\\udcc8 ' + esc(a['Signal']) + '\\n'\n  + (String(a['Evidence'] || '').trim() ? '\\ud83d\\udcce <i>' + esc(cut(a['Evidence'], 220)) + '</i>\\n' : '')\n  + '\\ud83d\\udcbc ' + a['Open roles'] + ' open \\u00b7 ' + esc(cut(a['Roles seen'], 120)) + '\\n\\n'\n  + (subject ? '<b>Subject</b>\\n' + esc(subject) + '\\n\\n' : '')\n  + '<b>Opener</b>\\n<pre>' + esc(body) + '</pre>\\n\\n'\n  + '\\u27a1 <a href=\"' + a['Example posting'] + '\">The posting this came from</a>';\n})() }}",
        "additionalFields": {
          "appendAttribution": false,
          "parse_mode": "HTML",
          "disable_web_page_preview": true
        }
      },
      "id": "hs22b701-0000-4000-8000-000000000025",
      "name": "Send the account brief to Telegram",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        3440,
        240
      ]
    }
  ],
  "connections": {
    "Every Monday at 8am": {
      "main": [
        [
          {
            "node": "Set your targets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set your targets": {
      "main": [
        [
          {
            "node": "Read your account list",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read your account list": {
      "main": [
        [
          {
            "node": "Collect the domains",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Collect the domains": {
      "main": [
        [
          {
            "node": "Find who they are hiring",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find who they are hiring": {
      "main": [
        [
          {
            "node": "Skip postings already read",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Skip postings already read": {
      "main": [
        [
          {
            "node": "Group postings by account",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Group postings by account": {
      "main": [
        [
          {
            "node": "Read the hiring signal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the hiring signal": {
      "main": [
        [
          {
            "node": "Format the account row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format the account row": {
      "main": [
        [
          {
            "node": "Save every account brief",
            "type": "main",
            "index": 0
          },
          {
            "node": "Keep the accounts worth working",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep the accounts worth working": {
      "main": [
        [
          {
            "node": "Cap the accounts per run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cap the accounts per run": {
      "main": [
        [
          {
            "node": "Write the outreach opener",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write the outreach opener": {
      "main": [
        [
          {
            "node": "Send the account brief to Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI model": {
      "ai_languageModel": [
        [
          {
            "node": "Read the hiring signal",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Write the outreach opener",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Signal format": {
      "ai_outputParser": [
        [
          {
            "node": "Read the hiring signal",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  }
}
