{
  "name": "Phase 3 - First Message",
  "nodes": [
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-4o-mini",
          "mode": "list",
          "cachedResultName": "GPT-4O-MINI"
        },
        "responses": {
          "values": [
            {
              "role": "system",
              "content": "You write the FIRST LinkedIn outreach message from a partnerships lead at Learn with Leaders (LwL). LwL partners with K-12 schools worldwide to bring student leadership and enrichment programs: World Leaders Academy, Edge Club, Future Doctors Fellowship, and TEO Launchpad. Write ONE short LinkedIn DM (about 40-60 words, 2-4 sentences). Tone: warm, professional, human, specific to this person and school. Reference their role and school naturally. Goal: open a short conversation. Make ONE clear, low-friction ask (e.g., whether they are open to a quick chat). Do NOT hard-sell, do NOT paste any link, do NOT invent facts about the school. No 'Dear', no subject line, no sign-off block - just the message. Return ONLY the message text."
            },
            {
              "content": "={{ $json.userPrompt }}"
            }
          ]
        },
        "builtInTools": {},
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2.3,
      "position": [
        1408,
        944
      ],
      "id": "836990d9-c146-4596-8d75-6f01897912e0",
      "name": "Message a model",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const out = [];\nfor (const item of items) {\n  const c = item.json;\n  const s = Array.isArray(c.schools) ? (c.schools[0] || {}) : (c.schools || {});\n  const userPrompt =\n    \"Write the first outreach message for:\\n\" +\n    \"- Contact name: \" + (c.name || 'there') + \"\\n\" +\n    \"- Role: \" + (c.role || 'school leader') + \"\\n\" +\n    \"- School: \" + (s.name || 'their school') + \"\\n\" +\n    \"- City: \" + (s.city || 'n/a') + \"\\n\" +\n    \"- Curriculum/board: \" + (s.board || 'n/a') + \"\\n\" +\n    \"(Internal only, do NOT mention: tier \" + (s.tier != null ? s.tier : 'n/a') + \".)\";\n  out.push({ json: { contactId: c.id, name: c.name, schoolName: s.name || null, userPrompt: userPrompt } });\n}\nreturn out;\n"
      },
      "id": "eb1a3124-db93-40b5-9403-ca7ca93fe2ba",
      "name": "Prep Fields",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        944
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Config').item.json.supabaseUrl + '/rest/v1/messages' }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "supabaseApi",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Prefer",
              "value": "return=minimal"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { \"contact_id\": $json.contactId, \"direction\": \"outbound\", \"body\": $json.messageBody, \"source\": \"ai\" } }}",
        "options": {}
      },
      "id": "a72ec124-b891-4923-934b-d45d6c87d9c2",
      "name": "Insert Message",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2160,
        816
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "d994ae54-58aa-449a-b9dc-89a1ada89d61",
              "name": "supabaseUrl",
              "value": "https://YOUR_PROJECT_REF.supabase.co",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "d040ecba-119f-466a-9b62-1d8713f2f3e4",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        768,
        944
      ]
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "={{ $('Config').item.json.supabaseUrl + '/rest/v1/contacts?id=eq.' + $json.contactId }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "supabaseApi",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Prefer",
              "value": "return=minimal"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { \"outreach_status\": \"message_sent\", \"last_contacted_at\": $now.toISO() } }}",
        "options": {}
      },
      "id": "dcead2f0-c16f-4337-af26-9606eecff666",
      "name": "Update Contact Status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2176,
        1040
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "url": "={{ $('Config').item.json.supabaseUrl + '/rest/v1/contacts?outreach_status=eq.not_contacted&select=id,name,role,school_id,schools(name,tier,board,city,tier_reason)&limit=5' }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "supabaseApi",
        "options": {}
      },
      "id": "7851e952-c6f6-4e58-ba17-7d362ede69dc",
      "name": "Get Not-Contacted",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        976,
        944
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const prep = $('Prep Fields').all();\n\nfunction extractText(r) {\n  if (r == null) return '';\n  if (typeof r === 'string') return r;\n  // OpenAI Responses API: output: [ { content: [ { type:'output_text', text } ] } ]\n  if (Array.isArray(r.output)) {\n    let acc = '';\n    for (const msg of r.output) {\n      if (msg && Array.isArray(msg.content)) {\n        for (const c of msg.content) { if (c && typeof c.text === 'string') acc += c.text; }\n      } else if (typeof msg === 'string') { acc += msg; }\n      else if (msg && typeof msg.text === 'string') { acc += msg.text; }\n    }\n    if (acc) return acc;\n  }\n  if (typeof r.output_text === 'string') return r.output_text;\n  if (typeof r.output === 'string') return r.output;\n  if (typeof r.content === 'string') return r.content;\n  if (typeof r.text === 'string') return r.text;\n  if (r.message && typeof r.message.content === 'string') return r.message.content;\n  if (r.choices && r.choices[0] && r.choices[0].message && typeof r.choices[0].message.content === 'string') return r.choices[0].message.content;\n  if (Array.isArray(r.content)) return r.content.map(b => (b && b.text) ? b.text : '').join('');\n  return '';\n}\n\nconst out = [];\nfor (let i = 0; i < items.length; i++) {\n  const text = extractText(items[i].json).trim();\n  const c = (prep[i] && prep[i].json) ? prep[i].json : {};\n  out.push({ json: { contactId: c.contactId, name: c.name, messageBody: text } });\n}\nreturn out;"
      },
      "id": "fea5d5e3-d52d-4717-a0f3-3f561310841d",
      "name": "Extract Message",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1776,
        944
      ]
    },
    {
      "parameters": {},
      "id": "4f1e7da5-cf7b-4d64-bbd6-3b9f4a2f99e8",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        548,
        944
      ]
    }
  ],
  "connections": {
    "Get Not-Contacted": {
      "main": [
        [
          {
            "node": "Prep Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prep Fields": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Message": {
      "main": [
        [
          {
            "node": "Insert Message",
            "type": "main",
            "index": 0
          },
          {
            "node": "Update Contact Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Get Not-Contacted",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Message a model": {
      "main": [
        [
          {
            "node": "Extract Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}