{
  "name": "Airtable Operations System: Leads, AI Agent, Outreach, Sync, Canva, Make/Zapier Bridge",
  "nodes": [
    {
      "id": "read-me-first-sticky",
      "name": "Read me first",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -60,
        -760
      ],
      "parameters": {
        "width": 720,
        "height": 460,
        "color": 7,
        "content": "## Harbourline Group operations system\nSix flows in one import. Airtable is the one place the data lives, n8n is the layer that moves it.\n\n1. LinkedIn and website leads land in Airtable, scored and routed\n2. A Claude agent that runs the task, not just the trigger\n3. Outreach written off each record, spaced and capped\n4. One clean list across Airtable, Sheets and Excel every night\n5. Campaign copy in Airtable becomes finished Canva artwork\n6. Make scenarios and Zaps point here so you can retire them one at a time\n\nWhat sits outside n8n: designing the Airtable base itself, the fields and views inside it, the Make and Zapier scenarios while they are still running, the LinkedIn connector that posts to Flow 1, and the Canva brand template that Flow 5 fills. Those are set up once in their own tools. n8n is the glue between them.\n\nEvery value on this canvas is demo data for Harbourline Group, a made-up company. This is a demonstration build, not a copy of anyone's production file. Point the credentials at real accounts and swap the base and file ids."
      }
    },
    {
      "id": "flow1-sticky",
      "name": "Flow 1 note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -60,
        -220
      ],
      "parameters": {
        "content": "## Flow 1: LinkedIn and website leads into Airtable\n\n- One webhook takes LinkedIn form leads, LinkedIn reply exports, and website form leads, and reads all three into the same shape.\n- Claude scores each lead, the row lands in the Airtable leads table next to its company, and the hot ones get a sales task plus a Slack post before the webhook answers.",
        "height": 200,
        "width": 460,
        "color": 4
      }
    },
    {
      "id": "flow1-webhook",
      "name": "New lead arrives",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        0,
        300
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "harbourline/new-lead",
        "responseMode": "responseNode",
        "options": {}
      }
    },
    {
      "id": "flow1-read-lead",
      "name": "Read the lead from any source",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        230,
        300
      ],
      "parameters": {
        "jsCode": "// ---- Settings you can change ----------------------------------\nconst SOURCE_LINKEDIN_FORM = 'linkedin-form';\nconst SOURCE_LINKEDIN_OUTREACH = 'linkedin-outreach';\nconst SOURCE_WEBSITE = 'website';\nconst DEFAULT_CAMPAIGN = 'always-on';\n\n// LinkedIn Lead Gen Forms name their answers. Map those names to ours.\nconst LINKEDIN_ANSWERS = {\n  emailAddress: 'email',\n  email: 'email',\n  firstName: 'first_name',\n  lastName: 'last_name',\n  company: 'company',\n  companyName: 'company',\n  jobTitle: 'job_title',\n  title: 'job_title',\n  phoneNumber: 'phone',\n  phone: 'phone',\n  linkedinProfile: 'linkedin_url',\n  message: 'message',\n};\n// ---------------------------------------------------------------\n\nfunction str(value) {\n  if (value === null || value === undefined) return '';\n  return String(value).trim();\n}\n\nfunction cleanEmail(value) {\n  return str(value).toLowerCase();\n}\n\nfunction cleanPhone(value) {\n  const digits = str(value).replace(/[^0-9]/g, '');\n  return digits ? '+' + digits : '';\n}\n\nfunction fullName(first, last, whole) {\n  const joined = [str(first), str(last)].filter(Boolean).join(' ');\n  return joined || str(whole);\n}\n\n// LinkedIn posts answers as [{ name: 'emailAddress', values: ['a@example.com'] }]\nfunction flattenLinkedInAnswers(answers) {\n  const flat = {};\n  for (const answer of answers) {\n    const key = LINKEDIN_ANSWERS[answer.name];\n    if (!key) continue;\n    const value = Array.isArray(answer.values) ? answer.values[0] : answer.value;\n    flat[key] = str(value);\n  }\n  return flat;\n}\n\nconst out = [];\n\nfor (const item of $input.all()) {\n  const raw = item.json || {};\n  const body = raw.body && typeof raw.body === 'object' ? raw.body : raw;\n  const answers = body.answers || body.formResponse?.answers;\n\n  let lead;\n\n  if (Array.isArray(answers)) {\n    // 1. LinkedIn Lead Gen Form\n    const flat = flattenLinkedInAnswers(answers);\n    lead = {\n      email: cleanEmail(flat.email),\n      full_name: fullName(flat.first_name, flat.last_name),\n      first_name: str(flat.first_name),\n      company: str(flat.company),\n      job_title: str(flat.job_title),\n      linkedin_url: str(flat.linkedin_url || body.leadProfileUrl),\n      phone: cleanPhone(flat.phone),\n      source: SOURCE_LINKEDIN_FORM,\n      campaign: str(body.campaignName || body.campaign) || DEFAULT_CAMPAIGN,\n      message: str(flat.message),\n    };\n  } else if (body.reply_text || body.connection_message || body.sales_nav_list) {\n    // 2. LinkedIn outreach reply exported from Sales Navigator\n    lead = {\n      email: cleanEmail(body.work_email || body.email),\n      full_name: fullName(body.first_name, body.last_name, body.name),\n      first_name: str(body.first_name) || str(body.name).split(' ')[0],\n      company: str(body.company_name || body.company),\n      job_title: str(body.headline || body.job_title),\n      linkedin_url: str(body.profile_url || body.linkedin_url),\n      phone: cleanPhone(body.phone),\n      source: SOURCE_LINKEDIN_OUTREACH,\n      campaign: str(body.sales_nav_list || body.campaign) || DEFAULT_CAMPAIGN,\n      message: str(body.reply_text || body.connection_message),\n    };\n  } else {\n    // 3. Website form\n    lead = {\n      email: cleanEmail(body.email),\n      full_name: fullName(body.first_name, body.last_name, body.name || body.full_name),\n      first_name: str(body.first_name) || str(body.name || body.full_name).split(' ')[0],\n      company: str(body.company),\n      job_title: str(body.job_title || body.role),\n      linkedin_url: str(body.linkedin_url),\n      phone: cleanPhone(body.phone),\n      source: SOURCE_WEBSITE,\n      campaign: str(body.utm_campaign || body.campaign) || DEFAULT_CAMPAIGN,\n      message: str(body.message || body.notes),\n    };\n  }\n\n  out.push({ json: lead });\n}\n\nreturn out;\n"
      }
    },
    {
      "id": "flow1-find-company",
      "name": "Find the company in Airtable",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        460,
        300
      ],
      "alwaysOutputData": true,
      "parameters": {
        "authentication": "airtableTokenApi",
        "resource": "record",
        "operation": "search",
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "appHarbourOps01"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "tblCompanies"
        },
        "filterByFormula": "=LOWER({Company Name}) = '{{ ($json.company || '').toLowerCase() }}'",
        "returnAll": false,
        "limit": 1,
        "options": {}
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "flow1-score-lead",
      "name": "Have Claude score the lead",
      "type": "@n8n/n8n-nodes-langchain.anthropic",
      "typeVersion": 1,
      "position": [
        690,
        300
      ],
      "parameters": {
        "resource": "text",
        "operation": "message",
        "modelId": {
          "__rl": true,
          "mode": "id",
          "value": "claude-sonnet-5"
        },
        "messages": {
          "values": [
            {
              "content": "=The lead:\n{{ JSON.stringify($('Read the lead from any source').first().json) }}\n\nThe matching company record from Airtable, empty if we have not worked with them before:\n{{ JSON.stringify($json) }}"
            }
          ]
        },
        "simplify": true,
        "options": {
          "system": "You score inbound leads for Harbourline Group, a B2B operations consultancy that builds Airtable and automation systems for operations teams.\n\nScore 0 to 100 on fit. A good fit runs its operations across several disconnected tools, has enough staff to feel that pain, and the person sits in a role that can approve a project. A poor fit is a student, a job seeker, a competing agency, or a company too small to have an operations problem worth paying to fix.\n\nReply with this JSON and nothing else:\n{\"score\": 0-100, \"tier\": \"hot|warm|cold\", \"segment\": \"...\", \"why\": \"one plain sentence\", \"opening_line\": \"one sentence a human could actually send\"}\n\ntier is hot at 70 and up, warm from 40 to 69, cold under 40.\nsegment is a short label for the kind of business, like \"logistics ops\" or \"agency ops\".\n\nHow to write the why and the opening_line:\n- Plain words, the way a person actually talks.\n- No emojis, no em dashes, no exclamation marks.\n- No corporate filler. Never write reaching out, circling back, leverage, solutions, excited to connect, or hope this finds you well.\n- Do not stack three things in a row for effect.\n- Do not end a sentence with an -ing clause that adds nothing.\n- The opening_line points at something specific about this person or their company. If you have nothing specific, write something plain and honest instead of guessing.",
          "maxTokens": 600
        }
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "flow1-shape-lead",
      "name": "Get the lead ready for Airtable",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        920,
        300
      ],
      "parameters": {
        "jsCode": "// ---- Settings you can change ----------------------------------\nconst HOT_SCORE = 70;\nconst HOT_TIER = 'hot';\nconst SALES_TRACK = 'sales';\nconst NURTURE_TRACK = 'nurture';\nconst NEW_STATUS = 'New';\n// ---------------------------------------------------------------\n\nconst lead = $('Read the lead from any source').first().json;\nconst company = $('Find the company in Airtable').first().json || {};\n\n// Claude hands back JSON inside a text field. Pull the text out wherever it sits.\nfunction claudeText(json) {\n  if (typeof json.content === 'string') return json.content;\n  if (Array.isArray(json.content)) return json.content.map((part) => part.text || '').join('');\n  if (typeof json.text === 'string') return json.text;\n  return '';\n}\n\nfunction readScoring(json) {\n  const text = claudeText(json);\n  const start = text.indexOf('{');\n  const end = text.lastIndexOf('}');\n  if (start === -1 || end === -1) return {};\n  try {\n    return JSON.parse(text.slice(start, end + 1));\n  } catch (e) {\n    return {};\n  }\n}\n\nconst scoring = readScoring($input.first().json);\nconst score = Number(scoring.score) || 0;\nconst tier = scoring.tier || (score >= HOT_SCORE ? HOT_TIER : 'cold');\n\nreturn [\n  {\n    json: {\n      email: lead.email,\n      full_name: lead.full_name,\n      first_name: lead.first_name,\n      company: lead.company,\n      company_record_id: company.id || '',\n      job_title: lead.job_title,\n      linkedin_url: lead.linkedin_url,\n      phone: lead.phone,\n      source: lead.source,\n      campaign: lead.campaign,\n      score: score,\n      tier: tier,\n      segment: scoring.segment || '',\n      why: scoring.why || '',\n      opening_line: scoring.opening_line || '',\n      track: tier === HOT_TIER ? SALES_TRACK : NURTURE_TRACK,\n      status: NEW_STATUS,\n      created: new Date().toISOString(),\n    },\n  },\n];\n"
      }
    },
    {
      "id": "flow1-save-lead",
      "name": "Save the lead in Airtable",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        1150,
        300
      ],
      "parameters": {
        "authentication": "airtableTokenApi",
        "resource": "record",
        "operation": "upsert",
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "appHarbourOps01"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "tblLeads"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Email"
          ],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false,
          "value": {
            "Email": "={{ $json.email }}",
            "Full Name": "={{ $json.full_name }}",
            "Company": "={{ $json.company }}",
            "Job Title": "={{ $json.job_title }}",
            "LinkedIn URL": "={{ $json.linkedin_url }}",
            "Phone": "={{ $json.phone }}",
            "Source": "={{ $json.source }}",
            "Campaign": "={{ $json.campaign }}",
            "Score": "={{ $json.score }}",
            "Tier": "={{ $json.tier }}",
            "Segment": "={{ $json.segment }}",
            "Why": "={{ $json.why }}",
            "Opening Line": "={{ $json.opening_line }}",
            "Track": "={{ $json.track }}",
            "Status": "={{ $json.status }}",
            "Created": "={{ $json.created }}"
          }
        },
        "options": {
          "typecast": true
        }
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "flow1-is-hot",
      "name": "Is this one hot",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1380,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "flow1-cond-tier",
              "leftValue": "={{ $('Get the lead ready for Airtable').first().json.tier }}",
              "rightValue": "hot",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            },
            {
              "id": "flow1-cond-score",
              "leftValue": "={{ $('Get the lead ready for Airtable').first().json.score }}",
              "rightValue": 70,
              "operator": {
                "type": "number",
                "operation": "gte"
              }
            }
          ]
        },
        "looseTypeValidation": true,
        "options": {}
      }
    },
    {
      "id": "flow1-sales-task",
      "name": "Add a follow up task for sales",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        1610,
        140
      ],
      "parameters": {
        "authentication": "airtableTokenApi",
        "resource": "record",
        "operation": "create",
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "appHarbourOps01"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "tblTasks"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false,
          "value": {
            "Task": "=Call {{ $('Get the lead ready for Airtable').first().json.full_name }} at {{ $('Get the lead ready for Airtable').first().json.company }}",
            "Owner Email": "sales@harbourline-group.example.com",
            "Due": "={{ $now.plus({ days: 1 }).toISO() }}",
            "Related Lead": "={{ $json.id }}"
          }
        },
        "options": {
          "typecast": true
        }
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "flow1-slack-sales",
      "name": "Tell sales in Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        1840,
        140
      ],
      "parameters": {
        "authentication": "accessToken",
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#new-leads"
        },
        "text": "=New hot lead: {{ $('Get the lead ready for Airtable').first().json.full_name }} at {{ $('Get the lead ready for Airtable').first().json.company }}\n{{ $('Get the lead ready for Airtable').first().json.job_title }}, came in from {{ $('Get the lead ready for Airtable').first().json.source }}\nScore {{ $('Get the lead ready for Airtable').first().json.score }} ({{ $('Get the lead ready for Airtable').first().json.segment }})\nWhy: {{ $('Get the lead ready for Airtable').first().json.why }}\nSomething to open with: {{ $('Get the lead ready for Airtable').first().json.opening_line }}\nProfile: {{ $('Get the lead ready for Airtable').first().json.linkedin_url }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "flow1-respond",
      "name": "Tell the sender we have it",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        2070,
        300
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ saved: true, track: $('Get the lead ready for Airtable').first().json.track }) }}",
        "options": {}
      }
    },
    {
      "id": "f2-note",
      "name": "Flow 2 note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -60,
        880
      ],
      "parameters": {
        "width": 460,
        "height": 200,
        "color": 4,
        "content": "## Flow 2: The agent that runs the task, not just the trigger\n\n- Send a plain English request to the webhook and Claude does the work itself in Airtable, Gmail, Slack, and Harbourline's own API.\n- You get back a written account of what it changed, and the same account is filed in the sync log."
      }
    },
    {
      "id": "f2-webhook",
      "name": "Ask the operations agent",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        1400
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "harbourline/ops-agent",
        "responseMode": "responseNode",
        "options": {}
      }
    },
    {
      "id": "f2-read",
      "name": "Read the instruction",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        230,
        1400
      ],
      "parameters": {
        "jsCode": "const results = [];\n\nfor (const item of $input.all()) {\n  const data = item.json.body || item.json;\n  const slack = data.event || {};\n\n  const instruction = data.instruction || data.text || data.message || slack.text || '';\n  const requester = data.requester_email || data.requester || data.email || slack.user_email || 'ops@harbourline-group.example.com';\n  const sessionKey = data.session_key || data.thread_id || slack.thread_ts || requester;\n\n  results.push({\n    json: {\n      instruction: String(instruction).trim(),\n      requester: String(requester).trim(),\n      session_key: String(sessionKey).trim(),\n    },\n  });\n}\n\nreturn results;"
      }
    },
    {
      "id": "f2-agent",
      "name": "Run the task",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        460,
        1400
      ],
      "parameters": {
        "promptType": "define",
        "text": "={{ $json.instruction }}",
        "options": {
          "systemMessage": "You are the operations assistant for Harbourline Group, a B2B operations consultancy. Requests reach you from Slack, from Airtable buttons, and from other automations. Your job is to finish the request, not to explain how it could be finished.\n\nWhat you are allowed to do:\n- Read lead records in Airtable.\n- Update lead records in Airtable.\n- Open a task in Airtable.\n- Send an email from ops@harbourline-group.example.com.\n- Post a message in Slack.\n- Call the Harbourline internal API for anything the other tools do not cover.\n\nHow to work:\n- Check Airtable before you state anything about a lead, a company, or a task. Look it up. Do not guess.\n- Never invent a record id, a price, or a date. If you do not have the value, go and find it, or say you do not have it.\n- Do the task. If the request says update the record, update the record. A summary of what you would have done is not an answer.\n- Send an email only when the request asks for one.\n- Post in Slack only when the request asks for that, or when the request names a channel.\n- When you are done, report what you actually changed. Name the records you touched.\n- If any part of the request failed, or you could not do it, say so plainly and say why. Do not cover it up.\n\nVoice for anything a person will read, in email, in Slack, or in your final answer:\n- Plain words and short sentences.\n- No corporate phrases, no emojis, no em dashes.\n- Never write the phrase \"reaching out\".\n- Write the way a colleague writes a quick note."
        }
      }
    },
    {
      "id": "f2-shape",
      "name": "Write down what the agent did",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        690,
        1400
      ],
      "parameters": {
        "jsCode": "const asked = $('Read the instruction').all();\nconst results = [];\nconst items = $input.all();\n\nfor (let i = 0; i < items.length; i++) {\n  const agent = items[i].json;\n  const source = (asked[i] || asked[0] || { json: {} }).json;\n  const answer = agent.output || agent.text || 'The agent ran but sent back no text.';\n\n  results.push({\n    json: {\n      instruction: source.instruction || '',\n      requester: source.requester || '',\n      answer: String(answer).trim(),\n      finished_at: new Date().toISOString(),\n    },\n  });\n}\n\nreturn results;"
      }
    },
    {
      "id": "f2-log",
      "name": "Log the run in Airtable",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        920,
        1400
      ],
      "parameters": {
        "resource": "record",
        "operation": "create",
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "appHarbourOps01"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "tblSyncLog"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Run Type": "ops-agent",
            "Instruction": "={{ $json.instruction }}",
            "Requester": "={{ $json.requester }}",
            "Result": "={{ $json.answer }}",
            "Finished": "={{ $json.finished_at }}"
          },
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f2-respond",
      "name": "Send the answer back",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        1150,
        1400
      ],
      "parameters": {
        "respondWith": "text",
        "responseBody": "={{ $('Write down what the agent did').item.json.answer }}",
        "options": {}
      }
    },
    {
      "id": "f2-claude",
      "name": "Claude runs the agent",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.5,
      "position": [
        120,
        1660
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-5",
          "cachedResultName": "claude-sonnet-5"
        },
        "options": {
          "maxTokensToSample": 4096,
          "temperature": 0.2
        }
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f2-memory",
      "name": "Remember the conversation",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "typeVersion": 1.4,
      "position": [
        290,
        1660
      ],
      "parameters": {
        "sessionIdType": "customKey",
        "sessionKey": "={{ $json.session_key }}",
        "contextWindowLength": 15
      }
    },
    {
      "id": "f2-at-search",
      "name": "Look up records in Airtable",
      "type": "n8n-nodes-base.airtableTool",
      "typeVersion": 2.2,
      "position": [
        460,
        1660
      ],
      "parameters": {
        "toolDescription": "Read lead records from the Harbourline leads table in Airtable. Use it to check what is already on file before you say anything about a lead. You can pass an Airtable filter formula to narrow the list, for example {Status} = 'New' or {Email} = 'jane@example.com'. Leave the formula empty to get the most recent leads.",
        "resource": "record",
        "operation": "search",
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "appHarbourOps01"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "tblLeads"
        },
        "filterByFormula": "={{ $fromAI('search_formula', \"an Airtable filter formula that narrows the leads, for example {Status} = 'New'. Send an empty string to list recent leads.\", 'string') }}",
        "limit": 20,
        "options": {}
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f2-at-update",
      "name": "Update a record in Airtable",
      "type": "n8n-nodes-base.airtableTool",
      "typeVersion": 2.2,
      "position": [
        630,
        1660
      ],
      "parameters": {
        "toolDescription": "Change a lead record in the Harbourline leads table. You need the Airtable record id, so look the lead up first. Only send the fields you actually want to change.",
        "resource": "record",
        "operation": "update",
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "appHarbourOps01"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "tblLeads"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "id": "={{ $fromAI('record_id', 'the Airtable record id of the lead to change, taken from a lookup', 'string') }}",
            "Status": "={{ $fromAI('status', 'the new status for the lead, for example New, Working, Qualified, or Closed', 'string') }}",
            "Owner": "={{ $fromAI('owner', 'the email of the person who now owns the lead', 'string') }}",
            "Notes": "={{ $fromAI('notes', 'a short note about what changed and why', 'string') }}"
          },
          "matchingColumns": [
            "id"
          ],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f2-at-task",
      "name": "Add a task in Airtable",
      "type": "n8n-nodes-base.airtableTool",
      "typeVersion": 2.2,
      "position": [
        800,
        1660
      ],
      "parameters": {
        "toolDescription": "Open a task in the Harbourline tasks table so a person picks up the follow up work. Use it when the request asks for something a human has to do.",
        "resource": "record",
        "operation": "create",
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "appHarbourOps01"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "tblTasks"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Task": "={{ $fromAI('task_name', 'a short title for the task, written plainly', 'string') }}",
            "Owner": "={{ $fromAI('task_owner', 'the email of the person who should do the task', 'string') }}",
            "Due": "={{ $fromAI('task_due', 'the due date in YYYY-MM-DD form, only if the request gives one', 'string') }}",
            "Notes": "={{ $fromAI('task_notes', 'what the person needs to know to do the task', 'string') }}"
          },
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f2-gmail",
      "name": "Send an email",
      "type": "n8n-nodes-base.gmailTool",
      "typeVersion": 2.2,
      "position": [
        970,
        1660
      ],
      "parameters": {
        "toolDescription": "Send an email from ops@harbourline-group.example.com. Only use this when the request asks for an email to go out. Never use it to confirm your own work.",
        "resource": "message",
        "operation": "send",
        "sendTo": "={{ $fromAI('email', 'the recipient email address', 'string') }}",
        "subject": "={{ $fromAI('subject', 'the subject line, plain and short', 'string') }}",
        "emailType": "text",
        "message": "={{ $fromAI('body', 'the body of the email in plain text, written the way a colleague writes', 'string') }}",
        "options": {
          "appendAttribution": false
        }
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f2-slack",
      "name": "Post in Slack",
      "type": "n8n-nodes-base.slackTool",
      "typeVersion": 2.3,
      "position": [
        1140,
        1660
      ],
      "parameters": {
        "toolDescription": "Post a message in a Harbourline Slack channel. The channels in use are #new-leads, #ops-daily, and #creative. Use it when the request asks for the team to be told something.",
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $fromAI('channel', 'the Slack channel to post in, for example #ops-daily', 'string') }}"
        },
        "text": "={{ $fromAI('slack_message', 'the message text, plain and short', 'string') }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f2-api",
      "name": "Call the client's own API",
      "type": "@n8n/n8n-nodes-langchain.toolHttpRequest",
      "typeVersion": 1.1,
      "position": [
        1310,
        1660
      ],
      "parameters": {
        "toolDescription": "Reach Harbourline's own internal systems at https://api.harbourline-group.example.com/v1. Use this for anything that has no ready made connector, such as the billing ledger, the project register, or the delivery tracker. Give it the path you want and the JSON body to send.",
        "method": "POST",
        "url": "https://api.harbourline-group.example.com/v1/{endpoint}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "model",
        "placeholderDefinitions": {
          "values": [
            {
              "name": "endpoint",
              "description": "the internal API path after /v1, for example projects, invoices/1042, or delivery/status",
              "type": "string"
            }
          ]
        },
        "optimizeResponse": false
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "flow3-sticky",
      "name": "Flow 3 note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -60,
        1980
      ],
      "parameters": {
        "content": "## Flow 3: Outreach that writes itself off the record\n\n- Every weekday morning this picks the leads who are actually due, has Claude write each email from that person's own record, and sends it.\n- Out comes a sent email, a stamped record so nobody gets the same touch twice, a call task once someone runs out of touches, and one Slack line in #ops-daily.",
        "height": 200,
        "width": 460,
        "color": 4
      }
    },
    {
      "id": "flow3-schedule",
      "name": "Every weekday at 8am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        2500
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1-5"
            }
          ]
        }
      }
    },
    {
      "id": "flow3-get-list",
      "name": "Get the outreach list from Airtable",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        230,
        2500
      ],
      "parameters": {
        "authentication": "airtableTokenApi",
        "resource": "record",
        "operation": "search",
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "appHarbourOps01"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "tblLeads"
        },
        "filterByFormula": "AND({Track} = 'outreach', {Status} != 'Unsubscribed', {Status} != 'Customer')",
        "returnAll": true,
        "options": {}
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "flow3-pick-due",
      "name": "Pick who is due today",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        2500
      ],
      "parameters": {
        "jsCode": "// ---- Settings you can change ----------------------------------\nconst TOUCH_SPACING_DAYS = 3;\nconst MAX_TOUCHES = 4;\nconst DAILY_SEND_CAP = 40;\nconst STOP_STATUSES = ['Replied', 'Unsubscribed', 'Customer', 'Do not contact'];\n// ---------------------------------------------------------------\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\nconst now = Date.now();\n\nfunction str(value) {\n  if (value === null || value === undefined) return '';\n  return String(value).trim();\n}\n\nfunction fieldsOf(item) {\n  // Airtable rows come back either flat or wrapped in a fields object.\n  const json = item.json || {};\n  return json.fields && typeof json.fields === 'object' ? json.fields : json;\n}\n\nfunction daysSince(value) {\n  const when = Date.parse(str(value));\n  if (!when) return null;\n  return (now - when) / DAY_MS;\n}\n\nconst due = [];\nlet held_back = 0;\nlet handed_over = 0;\n\nfor (const item of $input.all()) {\n  const row = fieldsOf(item);\n  const recordId = item.json.id || row.id || '';\n  const status = str(row.Status);\n  const touches = Number(row.Touches) || 0;\n  const waited = daysSince(row['Last Touch Sent']);\n\n  // Anyone who answered, opted out, bought, or asked us to stop is done.\n  if (STOP_STATUSES.includes(status)) {\n    held_back++;\n    continue;\n  }\n\n  // Too soon since the last one.\n  if (waited !== null && waited < TOUCH_SPACING_DAYS) {\n    held_back++;\n    continue;\n  }\n\n  // Out of touches. A person picks it up from here.\n  if (touches >= MAX_TOUCHES) {\n    handed_over++;\n    continue;\n  }\n\n  const email = str(row.Email).toLowerCase();\n  if (!email) {\n    held_back++;\n    continue;\n  }\n\n  due.push({\n    record_id: recordId,\n    email: email,\n    full_name: str(row['Full Name']),\n    first_name: str(row['Full Name']).split(' ')[0],\n    company: str(row.Company),\n    job_title: str(row['Job Title']),\n    source: str(row.Source),\n    segment: str(row.Segment),\n    why: str(row.Why),\n    touch_number: touches + 1,\n    last_subject: str(row['Last Subject']),\n    last_touch_sent: str(row['Last Touch Sent']),\n    waited_days: waited === null ? null : Math.round(waited),\n    handoff: touches + 1 >= MAX_TOUCHES,\n  });\n}\n\n// Oldest touched first, so nobody sits at the back of the line forever.\ndue.sort((a, b) => {\n  const left = Date.parse(a.last_touch_sent || '') || 0;\n  const right = Date.parse(b.last_touch_sent || '') || 0;\n  return left - right;\n});\n\nconst overflow = Math.max(0, due.length - DAILY_SEND_CAP);\nconst sending = due.slice(0, DAILY_SEND_CAP);\nheld_back += overflow;\n\nconst stats = {\n  considered: $input.all().length,\n  sending: sending.length,\n  held_back: held_back,\n  handed_over: handed_over,\n  final_touch_today: sending.filter((lead) => lead.touch_number >= MAX_TOUCHES).length,\n  max_touches: MAX_TOUCHES,\n  spacing_days: TOUCH_SPACING_DAYS,\n  daily_cap: DAILY_SEND_CAP,\n};\n\nreturn sending.map((lead) => ({ json: Object.assign({}, lead, { stats: stats }) }));\n"
      }
    },
    {
      "id": "flow3-write-email",
      "name": "Have Claude write each email",
      "type": "@n8n/n8n-nodes-langchain.anthropic",
      "typeVersion": 1,
      "position": [
        690,
        2500
      ],
      "parameters": {
        "resource": "text",
        "operation": "message",
        "modelId": {
          "__rl": true,
          "mode": "id",
          "value": "claude-sonnet-5"
        },
        "messages": {
          "values": [
            {
              "content": "=Touch number: {{ $json.touch_number }} of {{ $json.stats.max_touches }}\nName: {{ $json.full_name }}\nFirst name: {{ $json.first_name }}\nCompany: {{ $json.company }}\nJob title: {{ $json.job_title }}\nHow we got them: {{ $json.source }}\nKind of business: {{ $json.segment }}\nWhy we think they fit: {{ $json.why }}\nThe last email we sent them was about: {{ $json.last_subject || 'nothing yet, this is the first one' }}"
            }
          ]
        },
        "simplify": true,
        "options": {
          "system": "You write one short outreach email for Harbourline Group, a B2B operations consultancy. Harbourline takes a company whose work is scattered across Airtable, spreadsheets, and a pile of half connected apps, and puts it back together as one Airtable centred system with the automation running off it.\n\nYou are given one person and the touch number. The touch number decides what the email does:\n1. Say why you are writing them specifically.\n2. Name one specific thing you noticed about how their setup runs.\n3. Give one short proof point, a real result at a similar company.\n4. Close plainly and offer to go away.\n\nRules for the email:\n- 90 words maximum. Shorter is better.\n- One question at most.\n- No attachments.\n- No links except the booking link https://harbourline-group.example.com/call, and only use it when it fits.\n\nVOICE RULES:\n- Write the way you would text someone you respect. Plain words, short sentences.\n- No corporate phrases. No emojis. No em dashes.\n- Never write I hope this finds you well, reaching out, circling back, touching base, leverage, solutions, synergy, or excited to connect.\n- Do not stack three things in a row for effect.\n- Do not end a sentence with an -ing clause that adds nothing.\n- Sign off as Dan at Harbourline.\n\nReply with this JSON and nothing else:\n{\"subject\": \"...\", \"body\": \"...\"}\nThe subject is lowercase, under 6 words, and reads like a person typed it, not a campaign.",
          "maxTokens": 600
        }
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "flow3-ready-email",
      "name": "Get the email ready",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        920,
        2500
      ],
      "parameters": {
        "jsCode": "// ---- Settings you can change ----------------------------------\nconst FALLBACK_SUBJECT = 'quick question about your setup';\nconst SIGN_OFF = '\\n\\nDan\\nHarbourline Group';\nconst FALLBACK_SPACING_DAYS = 3;\n// ---------------------------------------------------------------\n\n// Claude hands back JSON inside a text field. Pull the text out wherever it sits.\nfunction claudeText(json) {\n  if (typeof json.content === 'string') return json.content;\n  if (Array.isArray(json.content)) return json.content.map((part) => part.text || '').join('');\n  if (typeof json.text === 'string') return json.text;\n  return '';\n}\n\nfunction readEmail(json) {\n  const text = claudeText(json);\n  const start = text.indexOf('{');\n  const end = text.lastIndexOf('}');\n  if (start !== -1 && end !== -1) {\n    try {\n      return JSON.parse(text.slice(start, end + 1));\n    } catch (e) {\n      // Fall through and use the raw text as the body.\n    }\n  }\n  return { subject: '', body: text };\n}\n\nconst leads = $('Pick who is due today').all();\nconst out = [];\n\n$input.all().forEach((item, index) => {\n  const lead = (leads[index] && leads[index].json) || {};\n  const written = readEmail(item.json || {});\n  const body = String(written.body || '').trim();\n  const spacing = (lead.stats && lead.stats.spacing_days) || FALLBACK_SPACING_DAYS;\n\n  out.push({\n    json: {\n      record_id: lead.record_id,\n      email: lead.email,\n      full_name: lead.full_name,\n      company: lead.company,\n      touch_number: lead.touch_number,\n      handoff: lead.handoff === true,\n      stats: lead.stats,\n      subject: String(written.subject || '').trim() || FALLBACK_SUBJECT,\n      body: body.endsWith('Harbourline Group') ? body : body + SIGN_OFF,\n      next_touch_due: new Date(Date.now() + (spacing * 24 * 60 * 60 * 1000)).toISOString(),\n    },\n  });\n});\n\nreturn out;\n"
      }
    },
    {
      "id": "flow3-send-email",
      "name": "Send the outreach email",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        1150,
        2500
      ],
      "parameters": {
        "resource": "message",
        "operation": "send",
        "sendTo": "={{ $json.email }}",
        "subject": "={{ $json.subject }}",
        "emailType": "text",
        "message": "={{ $json.body }}",
        "options": {}
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "flow3-stamp-touch",
      "name": "Stamp the touch on the record",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        1380,
        2500
      ],
      "parameters": {
        "authentication": "airtableTokenApi",
        "resource": "record",
        "operation": "update",
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "appHarbourOps01"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "tblLeads"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "id"
          ],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false,
          "value": {
            "id": "={{ $('Get the email ready').item.json.record_id }}",
            "Touches": "={{ $('Get the email ready').item.json.touch_number }}",
            "Last Touch Sent": "={{ $now.toISO() }}",
            "Last Subject": "={{ $('Get the email ready').item.json.subject }}",
            "Next Touch Due": "={{ $('Get the email ready').item.json.next_touch_due }}"
          }
        },
        "options": {
          "typecast": true
        }
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "flow3-last-touch",
      "name": "Was that the last touch",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1610,
        2500
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "flow3-cond-last-touch",
              "leftValue": "={{ $('Get the email ready').item.json.touch_number }}",
              "rightValue": "={{ $('Get the email ready').item.json.stats.max_touches }}",
              "operator": {
                "type": "number",
                "operation": "gte"
              }
            }
          ]
        },
        "looseTypeValidation": true,
        "options": {}
      }
    },
    {
      "id": "flow3-holdout-task",
      "name": "Add a call task for the holdouts",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        1840,
        2340
      ],
      "parameters": {
        "authentication": "airtableTokenApi",
        "resource": "record",
        "operation": "create",
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "appHarbourOps01"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "tblTasks"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false,
          "value": {
            "Task": "=Emails are done, give {{ $('Get the email ready').item.json.full_name }} at {{ $('Get the email ready').item.json.company }} a call",
            "Owner Email": "sales@harbourline-group.example.com",
            "Due": "={{ $now.plus({ days: 2 }).toISO() }}",
            "Related Lead": "={{ $('Get the email ready').item.json.record_id }}"
          }
        },
        "options": {
          "typecast": true
        }
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "flow3-sum-morning",
      "name": "Sum up the morning",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2070,
        2500
      ],
      "parameters": {
        "jsCode": "// ---- Settings you can change ----------------------------------\nconst CHANNEL_LABEL = 'This morning';\n// ---------------------------------------------------------------\n\nconst written = $('Get the email ready').all().map((item) => item.json);\nconst stats = (written[0] && written[0].stats) || {\n  considered: 0,\n  sending: 0,\n  held_back: 0,\n  handed_over: 0,\n  final_touch_today: 0,\n};\n\nconst sent = written.length;\nconst handed_over = stats.handed_over + stats.final_touch_today;\nconst names = written.slice(0, 5).map((item) => item.full_name + ' at ' + item.company);\n\nconst lines = [\n  CHANNEL_LABEL + ': ' + sent + ' outreach emails went out.',\n  stats.held_back + ' were left alone, either too soon or already answered.',\n];\n\nif (handed_over > 0) {\n  lines.push(handed_over + ' ran out of touches and are now a call task for sales.');\n}\n\nif (names.length) {\n  lines.push('Went to: ' + names.join(', ') + (sent > names.length ? ' and ' + (sent - names.length) + ' more.' : '.'));\n}\n\nreturn [\n  {\n    json: {\n      sent: sent,\n      held_back: stats.held_back,\n      handed_over: handed_over,\n      considered: stats.considered,\n      digest: lines.join('\\n'),\n    },\n  },\n];\n"
      }
    },
    {
      "id": "flow3-post-digest",
      "name": "Post the morning digest",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        2300,
        2500
      ],
      "parameters": {
        "authentication": "accessToken",
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#ops-daily"
        },
        "text": "={{ $json.digest }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f4-sticky",
      "name": "Flow 4 note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -60,
        2980
      ],
      "parameters": {
        "content": "## Flow 4: One clean list across Airtable, Sheets and Excel\n\n- Every night it pulls the Google Sheet and the Excel workbook into one clean set of rows in Airtable.\n- Both files get the agreed list back, and a short Doc records what changed.",
        "height": 200,
        "width": 460,
        "color": 4
      }
    },
    {
      "id": "f4-schedule",
      "name": "Every night at 2am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        3400
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "triggerAtHour": 2
            }
          ]
        }
      }
    },
    {
      "id": "f4-read-sheet",
      "name": "Read what people typed in the Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        230,
        3400
      ],
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "1HbLnMasterOpsSheetDemo0123456789abcdefg",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Intake",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f4-read-excel",
      "name": "Read the Excel workbook",
      "type": "n8n-nodes-base.microsoftExcel",
      "typeVersion": 2.2,
      "position": [
        460,
        3400
      ],
      "parameters": {
        "resource": "worksheet",
        "operation": "readRows",
        "workbook": {
          "__rl": true,
          "value": "01HARBOURLINEWORKBOOKDEMO7XK4",
          "mode": "id"
        },
        "worksheet": {
          "__rl": true,
          "value": "Leads",
          "mode": "id"
        },
        "options": {}
      },
      "credentials": {
        "microsoftExcelOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f4-clean",
      "name": "Clean both files into one list",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        690,
        3400
      ],
      "parameters": {
        "jsCode": "// ===== EDITABLE RULES =====\n// Left side is a spelling people actually type. Right side is the column it becomes.\nconst HEADER_ALIASES = {\n  'name': 'Name',\n  'full name': 'Name',\n  'contact name': 'Name',\n  'e-mail': 'Email',\n  'email': 'Email',\n  'email address': 'Email',\n  'co.': 'Company',\n  'company': 'Company',\n  'company name': 'Company',\n  'organisation': 'Company',\n  'organization': 'Company',\n  'phone': 'Phone',\n  'phone number': 'Phone',\n  'mobile': 'Phone',\n  'telephone': 'Phone',\n  'added': 'Added',\n  'date added': 'Added',\n  'created': 'Added',\n  'created at': 'Added'\n};\nconst DEFAULT_DIAL_CODE = '+1';\nconst KEEP_ROWS_WITHOUT_EMAIL = false;\n// ===== END EDITABLE RULES =====\n\nfunction canonicalKey(key) {\n  const flat = String(key).trim().toLowerCase().replace(/[_\\s]+/g, ' ');\n  return HEADER_ALIASES[flat] || String(key).trim();\n}\n\nfunction titleCase(value) {\n  return String(value === undefined || value === null ? '' : value)\n    .trim()\n    .replace(/\\s+/g, ' ')\n    .toLowerCase()\n    .replace(/(^|[\\s'-])([a-z])/g, (whole, sep, chr) => sep + chr.toUpperCase());\n}\n\nfunction cleanPhone(value) {\n  const original = String(value === undefined || value === null ? '' : value).trim();\n  const digits = original.replace(/[^0-9]/g, '');\n  if (!digits) return '';\n  if (original.startsWith('+')) return '+' + digits;\n  if (digits.length === 10) return DEFAULT_DIAL_CODE + digits;\n  return '+' + digits;\n}\n\nfunction cleanDate(value) {\n  if (!value) return '';\n  const parsed = new Date(value);\n  if (isNaN(parsed.getTime())) return '';\n  return parsed.toISOString();\n}\n\nfunction renameRow(row) {\n  const out = {};\n  for (const [key, value] of Object.entries(row)) {\n    out[canonicalKey(key)] = value;\n  }\n  return out;\n}\n\n// The Sheet rows are reached back through the node that read them, because this\n// step only has one input.\nconst fromSheet = $('Read what people typed in the Sheet').all().map(item => ({ row: renameRow(item.json), source: 'sheet' }));\nconst fromExcel = $input.all().map(item => ({ row: renameRow(item.json), source: 'excel' }));\n\nconst cleaned = [];\nfor (const entry of [...fromSheet, ...fromExcel]) {\n  const row = entry.row;\n  const email = String(row.Email === undefined || row.Email === null ? '' : row.Email).trim().toLowerCase();\n  if (!email && !KEEP_ROWS_WITHOUT_EMAIL) continue;\n  cleaned.push({\n    Name: titleCase(row.Name),\n    Email: email,\n    Phone: cleanPhone(row.Phone),\n    Company: titleCase(row.Company),\n    Added: cleanDate(row.Added),\n    Source: entry.source\n  });\n}\n\n// One row per email address. The newest wins.\nconst newest = new Map();\nfor (const row of cleaned) {\n  const seen = newest.get(row.Email);\n  if (!seen || (row.Added || '') >= (seen.Added || '')) newest.set(row.Email, row);\n}\n\nreturn [...newest.values()].map(row => ({ json: row }));\n"
      }
    },
    {
      "id": "f4-airtable-upsert",
      "name": "Write the clean rows into Airtable",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        920,
        3400
      ],
      "parameters": {
        "operation": "upsert",
        "base": {
          "__rl": true,
          "value": "appHarbourOps01",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "tblLeads",
          "mode": "id"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "matchingColumns": [
            "Email"
          ],
          "value": {},
          "schema": []
        },
        "options": {}
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f4-airtable-search",
      "name": "Read the agreed view back out",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        1150,
        3400
      ],
      "parameters": {
        "operation": "search",
        "base": {
          "__rl": true,
          "value": "appHarbourOps01",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "tblLeads",
          "mode": "id"
        },
        "returnAll": true,
        "options": {
          "fields": [
            "Name",
            "Email",
            "Phone",
            "Company",
            "Source",
            "Added"
          ]
        }
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f4-shape",
      "name": "Shape the rows for the two files",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1380,
        3400
      ],
      "parameters": {
        "jsCode": "// Columns the Sheet and the workbook are allowed to show, in this order.\nconst COLUMNS = ['Name', 'Email', 'Phone', 'Company', 'Source', 'Added'];\n\nconst rows = $input.all().map(item => item.json.fields || item.json);\n\nfunction shape(row) {\n  const out = {};\n  for (const column of COLUMNS) {\n    const value = row[column];\n    out[column] = value === undefined || value === null ? '' : value;\n  }\n  return out;\n}\n\n// Both files take the same shape. The Sheet step and the Excel step each match on\n// Email, so a flat row per record is all either one needs.\nconst forSheets = rows.map(shape);\nconst forExcel = rows.map(shape);\n\nreturn forSheets.map((row, index) => ({ json: { ...row, ...forExcel[index] } }));\n"
      }
    },
    {
      "id": "f4-push-sheet",
      "name": "Push the clean list back to the Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1610,
        3400
      ],
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "1HbLnMasterOpsSheetDemo0123456789abcdefg",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Master",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "matchingColumns": [
            "Email"
          ],
          "value": {},
          "schema": []
        },
        "options": {}
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f4-push-excel",
      "name": "Push the clean list back to Excel",
      "type": "n8n-nodes-base.microsoftExcel",
      "typeVersion": 2.2,
      "position": [
        1840,
        3400
      ],
      "parameters": {
        "resource": "worksheet",
        "operation": "upsert",
        "workbook": {
          "__rl": true,
          "value": "01HARBOURLINEWORKBOOKDEMO7XK4",
          "mode": "id"
        },
        "worksheet": {
          "__rl": true,
          "value": "Leads",
          "mode": "id"
        },
        "dataMode": "autoMap",
        "columnToMatchOn": "Email",
        "options": {}
      },
      "credentials": {
        "microsoftExcelOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f4-summary",
      "name": "Write up what changed",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2070,
        3400
      ],
      "parameters": {
        "jsCode": "const EMAIL_KEYS = ['email', 'e-mail', 'email address'];\n\nfunction emailOf(row) {\n  for (const [key, value] of Object.entries(row)) {\n    if (EMAIL_KEYS.includes(String(key).trim().toLowerCase().replace(/[_\\s]+/g, ' '))) {\n      return String(value === undefined || value === null ? '' : value).trim().toLowerCase();\n    }\n  }\n  return '';\n}\n\nconst sheetRows = $('Read what people typed in the Sheet').all().map(item => item.json);\nconst excelRows = $('Read the Excel workbook').all().map(item => item.json);\nconst cleanRows = $('Clean both files into one list').all().map(item => item.json);\n\nconst rawEmails = [...sheetRows, ...excelRows].map(emailOf);\nconst withEmail = rawEmails.filter(value => value !== '');\nconst rowsWithNoEmail = rawEmails.length - withEmail.length;\nconst repeatsDropped = withEmail.length - new Set(withEmail).size;\n\nconst runDate = $now.toFormat('yyyy-LL-dd');\n\nconst summary = [\n  'Nightly data sync for ' + runDate + '.',\n  '',\n  'Rows read from the Google Sheet: ' + sheetRows.length,\n  'Rows read from the Excel workbook: ' + excelRows.length,\n  'Rows written to Airtable: ' + cleanRows.length,\n  'Repeats dropped: ' + repeatsDropped,\n  'Rows with no email address: ' + rowsWithNoEmail,\n  '',\n  'Airtable holds the agreed list. The Sheet and the workbook now match it.'\n].join('\\n');\n\nreturn [{\n  json: {\n    runDate,\n    readFromSheet: sheetRows.length,\n    readFromExcel: excelRows.length,\n    written: cleanRows.length,\n    repeatsDropped,\n    rowsWithNoEmail,\n    summary\n  }\n}];\n"
      }
    },
    {
      "id": "f4-doc-create",
      "name": "File the nightly summary as a Doc",
      "type": "n8n-nodes-base.googleDocs",
      "typeVersion": 2,
      "position": [
        2300,
        3400
      ],
      "parameters": {
        "authentication": "oAuth2",
        "resource": "document",
        "operation": "create",
        "driveId": "myDrive",
        "folderId": "1HbLnBriefsFolderDemo0987654321",
        "title": "=Data sync {{ $now.toFormat('yyyy-LL-dd') }}"
      },
      "credentials": {
        "googleDocsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f4-doc-write",
      "name": "Put the summary into the Doc",
      "type": "n8n-nodes-base.googleDocs",
      "typeVersion": 2,
      "position": [
        2530,
        3400
      ],
      "parameters": {
        "authentication": "oAuth2",
        "resource": "document",
        "operation": "update",
        "documentURL": "={{ $json.documentId }}",
        "simple": true,
        "actionsUi": {
          "actionFields": [
            {
              "object": "text",
              "action": "insert",
              "insertSegment": "body",
              "locationChoice": "endOfSegmentLocation",
              "text": "={{ $('Write up what changed').first().json.summary }}"
            }
          ]
        },
        "updateFields": {}
      },
      "credentials": {
        "googleDocsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f4-airtable-log",
      "name": "Log the sync in Airtable",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        2760,
        3400
      ],
      "parameters": {
        "operation": "create",
        "base": {
          "__rl": true,
          "value": "appHarbourOps01",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "tblSyncLog",
          "mode": "id"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Run Type": "nightly-data-sync",
            "Rows From Sheet": "={{ $('Write up what changed').first().json.readFromSheet }}",
            "Rows From Excel": "={{ $('Write up what changed').first().json.readFromExcel }}",
            "Rows Written": "={{ $('Write up what changed').first().json.written }}",
            "Repeats Dropped": "={{ $('Write up what changed').first().json.repeatsDropped }}",
            "Rows Without Email": "={{ $('Write up what changed').first().json.rowsWithNoEmail }}",
            "Doc Link": "=https://docs.google.com/document/d/{{ $('File the nightly summary as a Doc').first().json.documentId }}/edit",
            "Finished": "={{ $now.toISO() }}"
          },
          "schema": []
        },
        "options": {}
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f5-sticky-note",
      "name": "Flow 5 note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -60,
        3980
      ],
      "parameters": {
        "width": 460,
        "height": 200,
        "color": 4,
        "content": "## Flow 5: Campaign copy in, finished Canva artwork out\n\n- Mark a campaign row ready and Canva builds the artwork from the brand template.\n- The PNG link lands back on the campaign row and in #creative."
      }
    },
    {
      "id": "f5-webhook-asset-request",
      "name": "Campaign is ready for artwork",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        0,
        4500
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "harbourline/asset-request",
        "responseMode": "responseNode",
        "options": {}
      }
    },
    {
      "id": "f5-airtable-read-campaign",
      "name": "Read the campaign row",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        230,
        4500
      ],
      "parameters": {
        "resource": "record",
        "operation": "get",
        "base": {
          "__rl": true,
          "value": "appHarbourOps01",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "tblCampaigns",
          "mode": "id"
        },
        "id": "={{ $json.body.recordId }}",
        "options": {}
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f5-code-pick-copy",
      "name": "Pick the copy for the artwork",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        4500
      ],
      "parameters": {
        "jsCode": "// ---------- EDITABLE RULES ----------\n// Campaign type in Airtable decides which Canva brand template gets filled.\nconst TEMPLATES = {\n  launch: 'DAGxHarbourLaunchTemplate',\n  webinar: 'DAGxHarbourWebinarTemplate'\n};\nconst DEFAULT_TEMPLATE = 'DAGxHarbourBrandTemplate';\n\n// Character limits so copy never overflows the template boxes.\nconst LIMITS = {\n  headline: 48,\n  subhead: 90,\n  cta: 24\n};\n// -------- END EDITABLE RULES --------\n\nfunction fit(value, max) {\n  return String(value || '').replace(/\\s+/g, ' ').trim().slice(0, max);\n}\n\nconst results = [];\n\nfor (const item of $input.all()) {\n  const row = item.json.fields || item.json;\n  const type = String(row['Campaign Type'] || '').trim().toLowerCase();\n\n  results.push({\n    json: {\n      campaignId: item.json.id || row['Record Id'] || '',\n      campaignName: fit(row['Name'] || 'Untitled campaign', 120),\n      campaignType: type || 'general',\n      brandTemplateId: TEMPLATES[type] || DEFAULT_TEMPLATE,\n      headline: fit(row['Headline'], LIMITS.headline),\n      subhead: fit(row['Subhead'], LIMITS.subhead),\n      cta: fit(row['Call To Action'], LIMITS.cta)\n    }\n  });\n}\n\nreturn results;"
      }
    },
    {
      "id": "f5-http-canva-autofill",
      "name": "Fill the Canva template",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        690,
        4500
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.canva.com/rest/v1/autofills",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "contentType": "json",
        "jsonBody": "={{ JSON.stringify({ brand_template_id: $json.brandTemplateId, title: $json.campaignName, data: { headline: { type: 'text', text: $json.headline }, subhead: { type: 'text', text: $json.subhead }, cta: { type: 'text', text: $json.cta } } }) }}",
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f5-wait-design",
      "name": "Give Canva a moment",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        920,
        4500
      ],
      "parameters": {
        "resume": "timeInterval",
        "amount": 20,
        "unit": "seconds"
      }
    },
    {
      "id": "f5-http-canva-autofill-status",
      "name": "Check if the design is done",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1150,
        4500
      ],
      "parameters": {
        "method": "GET",
        "url": "=https://api.canva.com/rest/v1/autofills/{{ $json.job.id }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f5-http-canva-export",
      "name": "Ask Canva for the PNG",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1380,
        4500
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.canva.com/rest/v1/exports",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "contentType": "json",
        "jsonBody": "={{ JSON.stringify({ design_id: $json.job.result.design.id, format: { type: 'png' } }) }}",
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f5-wait-export",
      "name": "Give the export a moment",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1610,
        4500
      ],
      "parameters": {
        "resume": "timeInterval",
        "amount": 15,
        "unit": "seconds"
      }
    },
    {
      "id": "f5-http-canva-export-status",
      "name": "Get the download link",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1840,
        4500
      ],
      "parameters": {
        "method": "GET",
        "url": "=https://api.canva.com/rest/v1/exports/{{ $json.job.id }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f5-code-tidy-asset",
      "name": "Tidy up the asset details",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2070,
        4500
      ],
      "parameters": {
        "jsCode": "// Flatten what Canva handed back so the Airtable nodes stay readable.\nconst copy = $('Pick the copy for the artwork').first().json;\nconst design = $('Check if the design is done').first().json.job.result.design;\n\nconst results = [];\n\nfor (const item of $input.all()) {\n  const job = item.json.job || {};\n  const links = job.urls || [];\n\n  results.push({\n    json: {\n      campaignId: copy.campaignId,\n      campaignName: copy.campaignName,\n      templateUsed: copy.brandTemplateId,\n      designUrl: design.url || '',\n      downloadUrl: links[0] || '',\n      createdAt: new Date().toISOString()\n    }\n  });\n}\n\nreturn results;"
      }
    },
    {
      "id": "f5-airtable-create-asset",
      "name": "File the artwork on the campaign",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        2300,
        4500
      ],
      "parameters": {
        "resource": "record",
        "operation": "create",
        "base": {
          "__rl": true,
          "value": "appHarbourOps01",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "tblAssets",
          "mode": "id"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Campaign": "={{ $json.campaignName }}",
            "Asset Type": "png",
            "Design Link": "={{ $json.designUrl }}",
            "Download Link": "={{ $json.downloadUrl }}",
            "Template": "={{ $json.templateUsed }}",
            "Created": "={{ $json.createdAt }}"
          },
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f5-airtable-update-campaign",
      "name": "Mark the campaign as arted up",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        2530,
        4500
      ],
      "parameters": {
        "resource": "record",
        "operation": "update",
        "base": {
          "__rl": true,
          "value": "appHarbourOps01",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "tblCampaigns",
          "mode": "id"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "id": "={{ $('Tidy up the asset details').item.json.campaignId }}",
            "Artwork Status": "Ready",
            "Artwork Link": "={{ $('Tidy up the asset details').item.json.downloadUrl }}"
          },
          "matchingColumns": [
            "id"
          ],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f5-slack-creative",
      "name": "Show the creative channel",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        2760,
        4500
      ],
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "#creative",
          "mode": "name"
        },
        "text": "=Artwork is ready for {{ $('Tidy up the asset details').item.json.campaignName }}. Download link: {{ $('Tidy up the asset details').item.json.downloadUrl }}",
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f5-respond-artwork",
      "name": "Hand back the artwork link",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        2990,
        4500
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ status: 'ready', campaign: $('Tidy up the asset details').item.json.campaignName, template: $('Tidy up the asset details').item.json.templateUsed, design_link: $('Tidy up the asset details').item.json.designUrl, download_link: $('Tidy up the asset details').item.json.downloadUrl }) }}",
        "options": {}
      }
    },
    {
      "id": "f6-sticky",
      "name": "Flow 6 note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -60,
        4980
      ],
      "parameters": {
        "width": 460,
        "height": 200,
        "color": 4,
        "content": "## Flow 6: Retire Make and Zapier one scenario at a time\n\n- Every old scenario posts to one n8n webhook, and n8n writes the record into the right Airtable table in the agreed shape.\n- Anything unrecognised gets parked instead of dropped, and a Monday message names the scenarios that are safe to switch off."
      }
    },
    {
      "id": "f6-webhook",
      "name": "Anything from Make or Zapier arrives",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        0,
        5500
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "harbourline/legacy-bridge",
        "authentication": "headerAuth",
        "responseMode": "responseNode",
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f6-intake",
      "name": "Work out what came in",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        230,
        5500
      ],
      "parameters": {
        "jsCode": "// ---- EDITABLE RULES ----\n// Record types the bridge is allowed to write. Anything else gets parked.\nconst ACCEPTED_TYPES = ['lead', 'company', 'campaign', 'task'];\n\n// Every Make scenario and Zap that has been pointed at this webhook.\nconst SOURCE_MAP = {\n  'make-crm-sync': 'make',\n  'make-invoice-watch': 'make',\n  'zap-typeform-intake': 'zapier',\n  'zap-calendly-booking': 'zapier'\n};\n\nconst FALLBACK_TYPE = 'unknown';\n// ---- END EDITABLE RULES ----\n\nconst results = [];\n\nfor (const item of $input.all()) {\n  const body = item.json.body || {};\n  const headers = item.json.headers || {};\n  const query = item.json.query || {};\n\n  // Make wraps a bundle as {\"1\": {...}}. Zapier posts a flat body. Both end up flat here.\n  let record = body;\n  const bundleKeys = Object.keys(body).filter((k) => /^\\d+$/.test(k));\n  if (bundleKeys.length && body[bundleKeys[0]] && typeof body[bundleKeys[0]] === 'object') {\n    record = body[bundleKeys[0]];\n  }\n\n  // Who sent it: header first, then the query string, then a source field in the body.\n  const scenario = String(\n    headers['x-automation-source'] || query.source || record.source || body.source || 'unlisted'\n  ).trim();\n\n  let platform = SOURCE_MAP[scenario] || '';\n  if (!platform) {\n    if (scenario.startsWith('make')) platform = 'make';\n    else if (scenario.startsWith('zap')) platform = 'zapier';\n    else platform = 'unknown';\n  }\n\n  let recordType = String(\n    record.type || record.record_type || body.type || body.record_type || FALLBACK_TYPE\n  ).toLowerCase().trim();\n  if (!ACCEPTED_TYPES.includes(recordType)) recordType = FALLBACK_TYPE;\n\n  const email = String(record.email || record.Email || '').toLowerCase().trim();\n\n  results.push({\n    json: {\n      platform,\n      scenario_name: scenario,\n      known_scenario: Boolean(SOURCE_MAP[scenario]),\n      record_type: recordType,\n      email,\n      name: record.name || record.full_name || '',\n      company_name: record.company || record.company_name || '',\n      campaign_name: record.campaign || record.campaign_name || '',\n      status: record.status || '',\n      owner: record.owner || '',\n      received: new Date().toISOString(),\n      raw: record\n    }\n  });\n}\n\nreturn results;\n"
      }
    },
    {
      "id": "f6-switch",
      "name": "Send it to the right table",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        460,
        5500
      ],
      "parameters": {
        "mode": "rules",
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "rule-lead",
                    "leftValue": "={{ $json.record_type }}",
                    "rightValue": "lead",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "lead"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "rule-company",
                    "leftValue": "={{ $json.record_type }}",
                    "rightValue": "company",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "company"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "rule-campaign",
                    "leftValue": "={{ $json.record_type }}",
                    "rightValue": "campaign",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "campaign"
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "anything else"
        }
      }
    },
    {
      "id": "f6-lead",
      "name": "Save it as a lead",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        690,
        5260
      ],
      "parameters": {
        "resource": "record",
        "operation": "upsert",
        "base": {
          "__rl": true,
          "value": "appHarbourOps01",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "tblLeads",
          "mode": "id"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Email": "={{ $json.email }}",
            "Full Name": "={{ $json.name }}",
            "Company": "={{ $json.company_name }}",
            "Status": "={{ $json.status || 'New' }}",
            "Owner": "={{ $json.owner }}",
            "Source": "={{ $json.scenario_name }}"
          },
          "matchingColumns": [
            "Email"
          ],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "typecast": true
        }
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f6-company",
      "name": "Save it as a company",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        690,
        5420
      ],
      "parameters": {
        "resource": "record",
        "operation": "upsert",
        "base": {
          "__rl": true,
          "value": "appHarbourOps01",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "tblCompanies",
          "mode": "id"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Company Name": "={{ $json.company_name || $json.name }}",
            "Primary Contact": "={{ $json.email }}",
            "Status": "={{ $json.status || 'Active' }}",
            "Source": "={{ $json.scenario_name }}"
          },
          "matchingColumns": [
            "Company Name"
          ],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "typecast": true
        }
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f6-campaign",
      "name": "Save it as a campaign",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        690,
        5580
      ],
      "parameters": {
        "resource": "record",
        "operation": "upsert",
        "base": {
          "__rl": true,
          "value": "appHarbourOps01",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "tblCampaigns",
          "mode": "id"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Campaign Name": "={{ $json.campaign_name || $json.name }}",
            "Owner": "={{ $json.owner }}",
            "Status": "={{ $json.status || 'Live' }}",
            "Source": "={{ $json.scenario_name }}"
          },
          "matchingColumns": [
            "Campaign Name"
          ],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "typecast": true
        }
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f6-park",
      "name": "Park anything we do not recognise",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        690,
        5760
      ],
      "parameters": {
        "resource": "record",
        "operation": "create",
        "base": {
          "__rl": true,
          "value": "appHarbourOps01",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "tblSyncLog",
          "mode": "id"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Run Type": "legacy-bridge-parked",
            "Platform": "={{ $json.platform }}",
            "Scenario Name": "={{ $json.scenario_name }}",
            "Record Type": "={{ $json.record_type }}",
            "Email": "={{ $json.email }}",
            "Received": "={{ $json.received }}",
            "Notes": "={{ JSON.stringify($json.raw) }}"
          },
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "typecast": true
        }
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f6-log",
      "name": "Log which scenario sent it",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        920,
        5500
      ],
      "parameters": {
        "resource": "record",
        "operation": "create",
        "base": {
          "__rl": true,
          "value": "appHarbourOps01",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "tblSyncLog",
          "mode": "id"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Run Type": "legacy-bridge",
            "Platform": "={{ $('Work out what came in').item.json.platform }}",
            "Scenario Name": "={{ $('Work out what came in').item.json.scenario_name }}",
            "Record Type": "={{ $('Work out what came in').item.json.record_type }}",
            "Email": "={{ $('Work out what came in').item.json.email }}",
            "Received": "={{ $('Work out what came in').item.json.received }}"
          },
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "typecast": true
        }
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f6-respond",
      "name": "Acknowledge the old scenario",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        1150,
        5500
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ received: true, type: $('Work out what came in').item.json.record_type, platform: $('Work out what came in').item.json.platform }) }}",
        "options": {}
      }
    },
    {
      "id": "f6-schedule",
      "name": "Every Monday at 9am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        5920
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 9,
              "triggerAtMinute": 0
            }
          ]
        }
      }
    },
    {
      "id": "f6-read-log",
      "name": "Read the last week of bridge traffic",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.2,
      "position": [
        230,
        5920
      ],
      "parameters": {
        "resource": "record",
        "operation": "search",
        "base": {
          "__rl": true,
          "value": "appHarbourOps01",
          "mode": "id"
        },
        "table": {
          "__rl": true,
          "value": "tblSyncLog",
          "mode": "id"
        },
        "filterByFormula": "AND({Run Type} = 'legacy-bridge', IS_AFTER({Received}, DATEADD(TODAY(), -7, 'days')))",
        "returnAll": true,
        "options": {}
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "f6-retire",
      "name": "Work out what can be switched off",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        5920
      ],
      "parameters": {
        "jsCode": "// ---- EDITABLE RULES ----\n// A scenario with no traffic for this many days is safe to switch off.\nconst QUIET_DAYS = 14;\n\n// Same list as the bridge SOURCE_MAP. Keep the two in step.\nconst KNOWN_SCENARIOS = [\n  'make-crm-sync',\n  'make-invoice-watch',\n  'zap-typeform-intake',\n  'zap-calendly-booking'\n];\n// ---- END EDITABLE RULES ----\n\nconst rows = $input.all().map((item) => item.json.fields || item.json);\n\nconst counts = {};\nfor (const row of rows) {\n  const name = String(row['Scenario Name'] || 'unlisted').trim();\n  counts[name] = (counts[name] || 0) + 1;\n}\n\nconst stillRunning = KNOWN_SCENARIOS\n  .filter((name) => counts[name])\n  .map((name) => name + ' (' + counts[name] + ')');\n\nconst wentQuiet = KNOWN_SCENARIOS.filter((name) => !counts[name]);\n\nconst unlisted = Object.keys(counts)\n  .filter((name) => !KNOWN_SCENARIOS.includes(name))\n  .map((name) => name + ' (' + counts[name] + ')');\n\nconst asText = (list) => (list.length ? list.join(', ') : 'nothing');\n\nreturn [{\n  json: {\n    quiet_days: QUIET_DAYS,\n    total_hits: rows.length,\n    still_running: stillRunning,\n    went_quiet: wentQuiet,\n    unlisted,\n    still_running_text: asText(stillRunning),\n    went_quiet_text: asText(wentQuiet),\n    unlisted_text: asText(unlisted)\n  }\n}];\n"
      }
    },
    {
      "id": "f6-slack",
      "name": "Post the migration progress",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        690,
        5920
      ],
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "#ops-daily",
          "mode": "name"
        },
        "text": "=Legacy bridge report for the last 7 days. {{ $json.total_hits }} records came through the webhook.\nStill sending: {{ $json.still_running_text }}\nNo traffic this week, safe to switch off once it has been quiet {{ $json.quiet_days }} days: {{ $json.went_quiet_text }}\nSent us data but not on our list: {{ $json.unlisted_text }}",
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "New lead arrives": {
      "main": [
        [
          {
            "node": "Read the lead from any source",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the lead from any source": {
      "main": [
        [
          {
            "node": "Find the company in Airtable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find the company in Airtable": {
      "main": [
        [
          {
            "node": "Have Claude score the lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Have Claude score the lead": {
      "main": [
        [
          {
            "node": "Get the lead ready for Airtable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get the lead ready for Airtable": {
      "main": [
        [
          {
            "node": "Save the lead in Airtable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save the lead in Airtable": {
      "main": [
        [
          {
            "node": "Is this one hot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is this one hot": {
      "main": [
        [
          {
            "node": "Add a follow up task for sales",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Tell the sender we have it",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add a follow up task for sales": {
      "main": [
        [
          {
            "node": "Tell sales in Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tell sales in Slack": {
      "main": [
        [
          {
            "node": "Tell the sender we have it",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask the operations agent": {
      "main": [
        [
          {
            "node": "Read the instruction",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the instruction": {
      "main": [
        [
          {
            "node": "Run the task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run the task": {
      "main": [
        [
          {
            "node": "Write down what the agent did",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write down what the agent did": {
      "main": [
        [
          {
            "node": "Log the run in Airtable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log the run in Airtable": {
      "main": [
        [
          {
            "node": "Send the answer back",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude runs the agent": {
      "ai_languageModel": [
        [
          {
            "node": "Run the task",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Remember the conversation": {
      "ai_memory": [
        [
          {
            "node": "Run the task",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Look up records in Airtable": {
      "ai_tool": [
        [
          {
            "node": "Run the task",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Update a record in Airtable": {
      "ai_tool": [
        [
          {
            "node": "Run the task",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Add a task in Airtable": {
      "ai_tool": [
        [
          {
            "node": "Run the task",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Send an email": {
      "ai_tool": [
        [
          {
            "node": "Run the task",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Post in Slack": {
      "ai_tool": [
        [
          {
            "node": "Run the task",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Call the client's own API": {
      "ai_tool": [
        [
          {
            "node": "Run the task",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Every weekday at 8am": {
      "main": [
        [
          {
            "node": "Get the outreach list from Airtable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get the outreach list from Airtable": {
      "main": [
        [
          {
            "node": "Pick who is due today",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick who is due today": {
      "main": [
        [
          {
            "node": "Have Claude write each email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Have Claude write each email": {
      "main": [
        [
          {
            "node": "Get the email ready",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get the email ready": {
      "main": [
        [
          {
            "node": "Send the outreach email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send the outreach email": {
      "main": [
        [
          {
            "node": "Stamp the touch on the record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Stamp the touch on the record": {
      "main": [
        [
          {
            "node": "Was that the last touch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Was that the last touch": {
      "main": [
        [
          {
            "node": "Add a call task for the holdouts",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Sum up the morning",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add a call task for the holdouts": {
      "main": [
        [
          {
            "node": "Sum up the morning",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sum up the morning": {
      "main": [
        [
          {
            "node": "Post the morning digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every night at 2am": {
      "main": [
        [
          {
            "node": "Read what people typed in the Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read what people typed in the Sheet": {
      "main": [
        [
          {
            "node": "Read the Excel workbook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the Excel workbook": {
      "main": [
        [
          {
            "node": "Clean both files into one list",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean both files into one list": {
      "main": [
        [
          {
            "node": "Write the clean rows into Airtable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write the clean rows into Airtable": {
      "main": [
        [
          {
            "node": "Read the agreed view back out",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the agreed view back out": {
      "main": [
        [
          {
            "node": "Shape the rows for the two files",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Shape the rows for the two files": {
      "main": [
        [
          {
            "node": "Push the clean list back to the Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Push the clean list back to the Sheet": {
      "main": [
        [
          {
            "node": "Push the clean list back to Excel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Push the clean list back to Excel": {
      "main": [
        [
          {
            "node": "Write up what changed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write up what changed": {
      "main": [
        [
          {
            "node": "File the nightly summary as a Doc",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "File the nightly summary as a Doc": {
      "main": [
        [
          {
            "node": "Put the summary into the Doc",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Put the summary into the Doc": {
      "main": [
        [
          {
            "node": "Log the sync in Airtable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Campaign is ready for artwork": {
      "main": [
        [
          {
            "node": "Read the campaign row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the campaign row": {
      "main": [
        [
          {
            "node": "Pick the copy for the artwork",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick the copy for the artwork": {
      "main": [
        [
          {
            "node": "Fill the Canva template",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fill the Canva template": {
      "main": [
        [
          {
            "node": "Give Canva a moment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Give Canva a moment": {
      "main": [
        [
          {
            "node": "Check if the design is done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check if the design is done": {
      "main": [
        [
          {
            "node": "Ask Canva for the PNG",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask Canva for the PNG": {
      "main": [
        [
          {
            "node": "Give the export a moment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Give the export a moment": {
      "main": [
        [
          {
            "node": "Get the download link",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get the download link": {
      "main": [
        [
          {
            "node": "Tidy up the asset details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tidy up the asset details": {
      "main": [
        [
          {
            "node": "File the artwork on the campaign",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "File the artwork on the campaign": {
      "main": [
        [
          {
            "node": "Mark the campaign as arted up",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark the campaign as arted up": {
      "main": [
        [
          {
            "node": "Show the creative channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Show the creative channel": {
      "main": [
        [
          {
            "node": "Hand back the artwork link",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anything from Make or Zapier arrives": {
      "main": [
        [
          {
            "node": "Work out what came in",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Work out what came in": {
      "main": [
        [
          {
            "node": "Send it to the right table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send it to the right table": {
      "main": [
        [
          {
            "node": "Save it as a lead",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Save it as a company",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Save it as a campaign",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Park anything we do not recognise",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save it as a lead": {
      "main": [
        [
          {
            "node": "Log which scenario sent it",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save it as a company": {
      "main": [
        [
          {
            "node": "Log which scenario sent it",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save it as a campaign": {
      "main": [
        [
          {
            "node": "Log which scenario sent it",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Park anything we do not recognise": {
      "main": [
        [
          {
            "node": "Log which scenario sent it",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log which scenario sent it": {
      "main": [
        [
          {
            "node": "Acknowledge the old scenario",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Monday at 9am": {
      "main": [
        [
          {
            "node": "Read the last week of bridge traffic",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the last week of bridge traffic": {
      "main": [
        [
          {
            "node": "Work out what can be switched off",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Work out what can be switched off": {
      "main": [
        [
          {
            "node": "Post the migration progress",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false
}