{
  "name": "04a — Outreach Touch 1 (Day 0)",
  "nodes": [
    {
      "id": "wf04a-0001",
      "name": "Weekdays 09:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        240,
        300
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9 * * 1-5"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "wf04a-0002",
      "name": "Read Send State",
      "type": "n8n-nodes-base.postgres",
      "position": [
        460,
        300
      ],
      "parameters": {
        "query": "SELECT value FROM leads.system_state WHERE key IN ('send_count_today','send_count_date','first_send_date','daily_cap')",
        "operation": "executeQuery",
        "additionalFields": {}
      },
      "typeVersion": 2.5
    },
    {
      "id": "wf04a-0003",
      "name": "Check Daily Cap",
      "type": "n8n-nodes-base.code",
      "position": [
        680,
        300
      ],
      "parameters": {
        "jsCode": "const rows = $input.all().map(i => i.json);\nconst state = {};\nfor (const row of rows) state[row.key] = row.value;\n\nconst today = new Date().toISOString().slice(0,10);\nconst rampDays = parseInt($env.SEND_CAP_RAMP_DAYS || '14');\nconst firstSend = state.first_send_date || '';\nlet cap = 40;\nif (firstSend) {\n  const daysSince = Math.floor((Date.now() - new Date(firstSend).getTime()) / 86400000);\n  if (daysSince >= rampDays) cap = 100;\n}\n\nlet countToday = 0;\nif (state.send_count_date === today) {\n  countToday = parseInt(state.send_count_today || '0');\n}\n\nreturn [{ json: { cap, countToday, today, at_cap: countToday >= cap } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "wf04a-0004",
      "name": "At Cap?",
      "type": "n8n-nodes-base.if",
      "position": [
        900,
        300
      ],
      "parameters": {
        "conditions": {
          "combinator": "and",
          "conditions": [
            {
              "id": "cap",
              "operator": {
                "type": "boolean",
                "operation": "equal"
              },
              "leftValue": "={{ $json.at_cap }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "wf04a-0005",
      "name": "Stop (daily cap reached)",
      "type": "n8n-nodes-base.noOp",
      "position": [
        1120,
        180
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "wf04a-0006",
      "name": "Fetch Sendable Leads",
      "type": "n8n-nodes-base.postgres",
      "position": [
        1120,
        420
      ],
      "parameters": {
        "query": "SELECT l.*, lc.id as contact_id, lc.full_name, lc.role_title, lc.email, lc.found_via\nFROM leads.leads l\nJOIN leads.lead_contacts lc ON lc.lead_id = l.id AND lc.is_primary = true\nWHERE l.state = 'qualified'\n  AND l.research_artifact IS NOT NULL\n  AND lc.email IS NOT NULL\n  AND lc.email_status IN ('verified_valid','catch_all','risky')\n  AND NOT EXISTS (\n    SELECT 1 FROM leads.email_sends es WHERE es.lead_id = l.id AND es.touch_number = 1\n  )\nORDER BY l.updated_at ASC\nLIMIT 10",
        "operation": "executeQuery",
        "additionalFields": {}
      },
      "typeVersion": 2.5
    },
    {
      "id": "wf04a-0008",
      "name": "Build Touch 1 Prompt",
      "type": "n8n-nodes-base.code",
      "position": [
        1560,
        420
      ],
      "parameters": {
        "jsCode": "const l = $json;\nconst ra = l.research_artifact || {};\nconst senderName = $env.SENDER_NAME || 'Artenis';\n\nconst question = ra.curiosity_question || ('How does your team handle ' + (ra.the_specific_workflow || 'this workflow') + ' today?');\n\nconst systemPrompt = `You are ${senderName}. Write a short, genuine email that opens a conversation — this is NOT a sales pitch.\n\nRecipient: ${l.full_name}, ${l.role_title} at ${l.company_name}\nTheir manual workflow: ${ra.the_specific_workflow}\nQuestion to ask: ${question}\n\nRULES:\n- Under 50 words total\n- One paragraph only\n- State one specific operational fact about their business (no \"I noticed\", no \"I saw\", no \"I came across\")\n- End with the curiosity question — make it feel like genuine professional interest, not a setup for a pitch\n- ZERO mention of your services, automation, software, or anything you sell\n- NO call to action, NO link, NO \"let me know if you need help\"\n- Sign off: just your first name on its own line\n\nSUBJECT: 4-6 words referencing their specific situation. Like \"ETA calls at ${l.company_name}\" or \"Review requests at ${l.company_name}\"\n\nBANNED: I noticed, I came across, I wanted to reach out, streamline, automate, solution, efficiency, I help, I build, I work with\n\nOUTPUT: valid JSON only — {\"subject\":\"...\",\"body\":\"...\"}\nUse \\\\n for line breaks in body.`;\n\nreturn [{ json: { ...l, groq_system: systemPrompt, groq_user: 'Write the email now. JSON only. Use \\\\n not real newlines.', _regen_count: 0 } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "wf04a-0009",
      "name": "Generate Email",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1780,
        420
      ],
      "parameters": {
        "url": "https://api.groq.com/openai/v1/chat/completions",
        "method": "POST",
        "options": {
          "timeout": 30000
        },
        "jsonBody": "={{ JSON.stringify({ model: 'llama-3.3-70b-versatile', messages: [{ role: 'system', content: $json.groq_system }, { role: 'user', content: $json.groq_user }], temperature: 0.4, max_tokens: 400 }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "wf04a-0010",
      "name": "Parse Email Output",
      "type": "n8n-nodes-base.code",
      "position": [
        2000,
        420
      ],
      "parameters": {
        "jsCode": "const response = $json;\nconst prev = $('Build Touch 1 Prompt').item.json;\nconst raw = response.choices?.[0]?.message?.content || '';\n\nlet subject = '';\nlet body = '';\n\n// Fix literal newlines inside JSON strings, then parse\nfunction fixAndParse(s) {\n  let inStr = false, esc = false, out = [];\n  for (const c of s) {\n    const code = c.charCodeAt(0);\n    if (esc) { out.push(c); esc = false; }\n    else if (code === 92) { out.push(c); esc = true; }\n    else if (code === 34) { inStr = !inStr; out.push(c); }\n    else if (code === 10 && inStr) { out.push('\\\\n'); }\n    else { out.push(c); }\n  }\n  return JSON.parse(out.join(''));\n}\n\ntry {\n  const m = raw.match(/\\{[\\s\\S]*?\"subject\"[\\s\\S]*?\"body\"[\\s\\S]*?\\}/);\n  if (m) {\n    const parsed = fixAndParse(m[0]);\n    subject = (parsed.subject || '').trim();\n    body = (parsed.body || '').replace(/\\\\n/g, '\\n').trim();\n  }\n} catch(e) {}\n\n// Fallback: text format\nif (!subject || !body) {\n  const lines = raw.trim().split('\\n');\n  for (let i = 0; i < lines.length; i++) {\n    if (lines[i].toLowerCase().startsWith('subject:')) {\n      subject = lines[i].replace(/^subject:\\s*/i, '').trim();\n    } else if (subject && lines[i].trim() === '') {\n      body = lines.slice(i + 1).join('\\n').trim();\n      break;\n    }\n  }\n}\n\nif (!subject || !body) {\n  const lines = raw.trim().split('\\n');\n  subject = lines[0].trim();\n  body = lines.slice(1).join('\\n').trim();\n}\n\nconst isNH = subject === 'NEEDS_HUMAN' || body === 'NEEDS_HUMAN';\nreturn [{ json: { ...prev, generated_subject: subject, generated_body: isNH ? 'NEEDS_HUMAN' : body, raw_output: raw } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "wf04a-0011",
      "name": "NEEDS_HUMAN?",
      "type": "n8n-nodes-base.if",
      "position": [
        2220,
        420
      ],
      "parameters": {
        "conditions": {
          "combinator": "and",
          "conditions": [
            {
              "id": "nh",
              "operator": {
                "type": "string",
                "operation": "equal"
              },
              "leftValue": "={{ $json.generated_body }}",
              "rightValue": "NEEDS_HUMAN"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "wf04a-0012",
      "name": "Telegram: NEEDS_HUMAN Alert",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2440,
        280
      ],
      "parameters": {
        "url": "http://telegram-worker:8001/send",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ reply_id: 'nh-' + $json.id, text: '⚠️ NEEDS_HUMAN\\n\\nLead: ' + $json.company_name + ' (' + $json.domain + ')\\nContact: ' + $json.full_name + ' · ' + $json.role_title + '\\n\\nThe LLM could not produce a specific email. Manual write needed.' }) }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "wf04a-0013",
      "name": "Validate: No-Money + No-AI-Tells",
      "type": "n8n-nodes-base.code",
      "position": [
        2440,
        560
      ],
      "parameters": {
        "jsCode": "const body = $json.generated_body || '';\nconst subject = $json.generated_subject || '';\nconst combined = subject + ' ' + body;\n\nconst moneyRegex = /\\b(price|pricing|cost|costs|budget|rate|rates|fee|fees|hour|hourly|package|affordable|cheap)\\b|[€$£]|\\b\\d{2,}\\s*(eur|usd|gbp|lek)\\b/i;\nconst aiTellsRegex = /\\b(I hope this (email|message) finds you well|just (touching|checking|circling) (in|back)|leverage|synergy|streamline your|revolutionize|game[- ]?changer|cutting[- ]?edge|harness the power|unlock the potential|in today.s (fast-paced|digital|competitive)|delve into|tapestry|navigate the|landscape of|I noticed|I came across|I wanted to reach out|reaching out to you|I.d love to|no commitment)\\b/i;\n\nconst moneyFail = moneyRegex.test(combined);\nconst aiTellsFail = aiTellsRegex.test(combined);\nconst failed = moneyFail || aiTellsFail;\n\nreturn [{ json: { ...$json, validation_passed: !failed, money_fail: moneyFail, ai_tells_fail: aiTellsFail } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "wf04a-0014",
      "name": "Validation Passed?",
      "type": "n8n-nodes-base.if",
      "position": [
        2660,
        560
      ],
      "parameters": {
        "conditions": {
          "combinator": "and",
          "conditions": [
            {
              "id": "val",
              "operator": {
                "type": "boolean",
                "operation": "equal"
              },
              "leftValue": "={{ $json.validation_passed }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "wf04a-0015",
      "name": "Can Regen?",
      "type": "n8n-nodes-base.if",
      "position": [
        2880,
        420
      ],
      "parameters": {
        "conditions": {
          "combinator": "and",
          "conditions": [
            {
              "id": "regen",
              "operator": {
                "type": "number",
                "operation": "lt"
              },
              "leftValue": "={{ $json._regen_count }}",
              "rightValue": 1
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "wf04a-0016",
      "name": "Increment Regen Counter",
      "type": "n8n-nodes-base.code",
      "position": [
        3100,
        300
      ],
      "parameters": {
        "jsCode": "return [{ json: { ...$json, _regen_count: ($json._regen_count || 0) + 1 } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "wf04a-0017",
      "name": "Telegram: Escalate Validation Fail",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3100,
        560
      ],
      "parameters": {
        "url": "http://telegram-worker:8001/send",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ reply_id: 'val-fail-' + $json.id, text: '⚠️ Validation failed twice\\n\\n' + $json.company_name + '\\nMoney fail: ' + $json.money_fail + '\\nAI tells fail: ' + $json.ai_tells_fail + '\\n\\nGenerated body:\\n' + $json.generated_body }) }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "wf04a-0018",
      "name": "Append Signature",
      "type": "n8n-nodes-base.code",
      "position": [
        2880,
        700
      ],
      "parameters": {
        "jsCode": "const senderName = $env.SENDER_NAME || 'Your Name';\nconst senderEmail = $env.SENDER_EMAIL || 'you@example.com';\nconst body = $json.generated_body;\nconst signature = `\\n\\n${senderName}\\n${senderEmail}`;\nreturn [{ json: { ...$json, final_body: body + signature } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "wf04a-0019",
      "name": "Send via Gmail",
      "type": "n8n-nodes-base.gmail",
      "position": [
        3100,
        700
      ],
      "parameters": {
        "sendTo": "={{ $json.email }}",
        "message": "={{ $json.final_body }}",
        "options": {
          "appendAttribution": false
        },
        "subject": "={{ $json.generated_subject }}",
        "emailType": "text"
      },
      "typeVersion": 2.1
    },
    {
      "id": "wf04a-0020",
      "name": "Insert email_sends",
      "type": "n8n-nodes-base.postgres",
      "position": [
        3320,
        700
      ],
      "parameters": {
        "query": "INSERT INTO leads.email_sends (lead_id, contact_id, touch_number, subject, body, sent_at, gmail_message_id, gmail_thread_id)\nVALUES ($1, $2, 1, $3, $4, NOW(), $5, $6)",
        "options": {
          "queryReplacement": "={{ [$('Append Signature').item.json.id, $('Append Signature').item.json.contact_id, $('Append Signature').item.json.generated_subject, $('Append Signature').item.json.final_body, $json.id || null, $json.threadId || null] }}"
        },
        "operation": "executeQuery"
      },
      "typeVersion": 2.5
    },
    {
      "id": "wf04a-0021",
      "name": "Update Lead: Contacted",
      "type": "n8n-nodes-base.postgres",
      "position": [
        3540,
        700
      ],
      "parameters": {
        "query": "UPDATE leads.leads SET state = 'contacted', updated_at = NOW() WHERE id = $1",
        "options": {
          "queryReplacement": "={{ [$('Append Signature').item.json.id] }}"
        },
        "operation": "executeQuery"
      },
      "typeVersion": 2.5
    },
    {
      "id": "wf04a-0022",
      "name": "Increment Send Counter",
      "type": "n8n-nodes-base.postgres",
      "position": [
        3760,
        700
      ],
      "parameters": {
        "query": "INSERT INTO leads.system_state (key, value)\nVALUES ('send_count_today', ($1::int + 1)::text), ('send_count_date', $2),\n       ('first_send_date', CASE WHEN $3 = '' THEN $2 ELSE $3 END)\nON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value",
        "options": {
          "queryReplacement": "={{ [$('Check Daily Cap').item.json.countToday, $('Check Daily Cap').item.json.today, ''] }}"
        },
        "operation": "executeQuery"
      },
      "typeVersion": 2.5
    },
    {
      "id": "wf04a-0023",
      "name": "Telegram: Email Sent",
      "type": "n8n-nodes-base.telegram",
      "position": [
        3540,
        900
      ],
      "parameters": {
        "text": "Email sent ✅\n\nCompany: {{ $('Append Signature').item.json.company_name }} ({{ $('Append Signature').item.json.domain }})\nContact: {{ $('Append Signature').item.json.full_name }} · {{ $('Append Signature').item.json.role_title }}\nTo: {{ $('Append Signature').item.json.email }}\n\nSubject: {{ $('Append Signature').item.json.generated_subject }}\n\n{{ $('Append Signature').item.json.final_body.split('\\n').slice(0,4).join('\\n') }}",
        "chatId": "={{ $env.TELEGRAM_CHAT_ID }}",
        "additionalFields": {}
      },
      "typeVersion": 1.2
    }
  ],
  "connections": {
    "Weekdays 09:00": {
      "main": [
        [
          {
            "node": "Read Send State",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Send State": {
      "main": [
        [
          {
            "node": "Check Daily Cap",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Daily Cap": {
      "main": [
        [
          {
            "node": "At Cap?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "At Cap?": {
      "main": [
        [
          {
            "node": "Stop (daily cap reached)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fetch Sendable Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Sendable Leads": {
      "main": [
        [
          {
            "node": "Build Touch 1 Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Touch 1 Prompt": {
      "main": [
        [
          {
            "node": "Generate Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Email": {
      "main": [
        [
          {
            "node": "Parse Email Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Email Output": {
      "main": [
        [
          {
            "node": "NEEDS_HUMAN?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "NEEDS_HUMAN?": {
      "main": [
        [
          {
            "node": "Telegram: NEEDS_HUMAN Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Validate: No-Money + No-AI-Tells",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram: NEEDS_HUMAN Alert": {
      "main": [
        [
          {
            "node": "Split Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate: No-Money + No-AI-Tells": {
      "main": [
        [
          {
            "node": "Validation Passed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validation Passed?": {
      "main": [
        [
          {
            "node": "Can Regen?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Append Signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Can Regen?": {
      "main": [
        [
          {
            "node": "Increment Regen Counter",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Telegram: Escalate Validation Fail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Increment Regen Counter": {
      "main": [
        [
          {
            "node": "Generate Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram: Escalate Validation Fail": {
      "main": [
        [
          {
            "node": "Split Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append Signature": {
      "main": [
        [
          {
            "node": "Send via Gmail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send via Gmail": {
      "main": [
        [
          {
            "node": "Insert email_sends",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Insert email_sends": {
      "main": [
        [
          {
            "node": "Update Lead: Contacted",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Lead: Contacted": {
      "main": [
        [
          {
            "node": "Telegram: Email Sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Increment Send Counter": {
      "main": [
        [
          {
            "node": "Telegram: Email Sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram: Email Sent": {
      "main": [
        [
          {
            "node": "Increment Send Counter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
