From 5b87103fdb15f1817e45db6052c4469ef5121802 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sat, 7 Feb 2026 14:20:28 +0000 Subject: [PATCH] Add 5 n8n CRM automation workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 01: Contact intake webhook → Twenty CRM - 02: Lead nurturing 3-email sequence via Resend - 03: Daily CRM → Listmonk newsletter sync - 04: Weekly stale contact follow-up reminders - 05: Gitea/GitHub webhook events → CRM activity log Co-Authored-By: Claude Opus 4.6 --- n8n-workflows/01-contact-intake.json | 207 +++++++++++++ n8n-workflows/02-lead-nurturing.json | 278 +++++++++++++++++ n8n-workflows/03-newsletter-sync.json | 284 ++++++++++++++++++ n8n-workflows/04-follow-up-reminders.json | 237 +++++++++++++++ n8n-workflows/05-webhook-events.json | 347 ++++++++++++++++++++++ n8n-workflows/README.md | 64 ++++ 6 files changed, 1417 insertions(+) create mode 100644 n8n-workflows/01-contact-intake.json create mode 100644 n8n-workflows/02-lead-nurturing.json create mode 100644 n8n-workflows/03-newsletter-sync.json create mode 100644 n8n-workflows/04-follow-up-reminders.json create mode 100644 n8n-workflows/05-webhook-events.json create mode 100644 n8n-workflows/README.md diff --git a/n8n-workflows/01-contact-intake.json b/n8n-workflows/01-contact-intake.json new file mode 100644 index 0000000..f5299b0 --- /dev/null +++ b/n8n-workflows/01-contact-intake.json @@ -0,0 +1,207 @@ +{ + "name": "Contact Intake — Form to CRM", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "contact-intake", + "responseMode": "responseNode", + "options": {} + }, + "id": "webhook-trigger", + "name": "Webhook — Contact Form", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [240, 300], + "webhookId": "contact-intake" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "check-email", + "leftValue": "={{ $json.body.email }}", + "rightValue": "", + "operator": { + "type": "string", + "operation": "isNotEmpty" + } + }, + { + "id": "check-name", + "leftValue": "={{ $json.body.name }}", + "rightValue": "", + "operator": { + "type": "string", + "operation": "isNotEmpty" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "validate-input", + "name": "Validate Input", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [460, 300] + }, + { + "parameters": { + "method": "POST", + "url": "=https://crm.cosmolocal.world/api/v1/objects/people", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "Bearer {{ $env.TWENTY_API_KEY }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"name\": {\n \"firstName\": \"{{ $json.body.name.split(' ')[0] }}\",\n \"lastName\": \"{{ $json.body.name.split(' ').slice(1).join(' ') || '' }}\"\n },\n \"emails\": {\n \"primaryEmail\": \"{{ $json.body.email }}\"\n },\n \"phones\": {\n \"primaryPhone\": \"{{ $json.body.phone || '' }}\"\n }\n}", + "options": {} + }, + "id": "create-crm-contact", + "name": "Create Contact in Twenty CRM", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [680, 240] + }, + { + "parameters": { + "method": "POST", + "url": "=https://crm.cosmolocal.world/api/v1/objects/notes", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "Bearer {{ $env.TWENTY_API_KEY }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"title\": \"Contact Form Submission\",\n \"body\": \"{{ $('Webhook — Contact Form').item.json.body.message || 'No message provided' }}\",\n \"noteTargets\": [{\n \"personId\": \"{{ $json.data.id }}\"\n }]\n}", + "options": {} + }, + "id": "add-note", + "name": "Add Note to Contact", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [900, 240] + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={ \"success\": true, \"message\": \"Contact created successfully\" }", + "options": { + "responseCode": 200 + } + }, + "id": "respond-success", + "name": "Respond — Success", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "position": [1120, 240] + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={ \"success\": false, \"message\": \"Missing required fields: name and email\" }", + "options": { + "responseCode": 400 + } + }, + "id": "respond-error", + "name": "Respond — Validation Error", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "position": [680, 420] + } + ], + "connections": { + "Webhook — Contact Form": { + "main": [ + [ + { + "node": "Validate Input", + "type": "main", + "index": 0 + } + ] + ] + }, + "Validate Input": { + "main": [ + [ + { + "node": "Create Contact in Twenty CRM", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Respond — Validation Error", + "type": "main", + "index": 0 + } + ] + ] + }, + "Create Contact in Twenty CRM": { + "main": [ + [ + { + "node": "Add Note to Contact", + "type": "main", + "index": 0 + } + ] + ] + }, + "Add Note to Contact": { + "main": [ + [ + { + "node": "Respond — Success", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + }, + "staticData": null, + "tags": [ + { + "name": "cosmolocal" + }, + { + "name": "crm" + } + ], + "pinData": {} +} diff --git a/n8n-workflows/02-lead-nurturing.json b/n8n-workflows/02-lead-nurturing.json new file mode 100644 index 0000000..e0fd98d --- /dev/null +++ b/n8n-workflows/02-lead-nurturing.json @@ -0,0 +1,278 @@ +{ + "name": "Lead Nurturing — Welcome Email Sequence", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "new-contact-created", + "options": {} + }, + "id": "webhook-new-contact", + "name": "Webhook — New Contact Created", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [240, 300], + "webhookId": "new-contact-created" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "set-email", + "name": "email", + "value": "={{ $json.body.email || $json.body.emails?.primaryEmail }}", + "type": "string" + }, + { + "id": "set-first-name", + "name": "firstName", + "value": "={{ $json.body.firstName || $json.body.name?.firstName || 'there' }}", + "type": "string" + }, + { + "id": "set-contact-id", + "name": "contactId", + "value": "={{ $json.body.id || $json.body.contactId }}", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "extract-fields", + "name": "Extract Contact Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [460, 300] + }, + { + "parameters": { + "method": "POST", + "url": "https://api.resend.com/emails", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "Bearer {{ $env.RESEND_API_KEY }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"from\": \"Cosmolocal Foundation <***REDACTED_EMAIL***>\",\n \"to\": [\"{{ $json.email }}\"],\n \"subject\": \"Welcome to the Cosmolocal Foundation\",\n \"html\": \"

Welcome, {{ $json.firstName }}!

Thank you for connecting with the Cosmolocal Foundation. We're building a world where local communities thrive within regenerative economies, connected through global knowledge-sharing and commons-based collaboration.

\\\"What is heavy should be local, and what is light should be global and shared.\\\"

Here's what we're working on:

We'll be in touch with updates on our progress and opportunities to get involved.

Warm regards,
The Cosmolocal Foundation Team

cosmolocal.world

\"\n}", + "options": {} + }, + "id": "send-welcome-email", + "name": "Send Welcome Email (Day 0)", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [680, 300] + }, + { + "parameters": { + "amount": 3, + "unit": "days" + }, + "id": "wait-3-days", + "name": "Wait 3 Days", + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [900, 300] + }, + { + "parameters": { + "method": "POST", + "url": "https://api.resend.com/emails", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "Bearer {{ $env.RESEND_API_KEY }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"from\": \"Cosmolocal Foundation <***REDACTED_EMAIL***>\",\n \"to\": [\"{{ $('Extract Contact Fields').item.json.email }}\"],\n \"subject\": \"Our Strategic Priorities — How We're Building Change\",\n \"html\": \"

Hi {{ $('Extract Contact Fields').item.json.firstName }},

We wanted to share more about our strategic approach to systemic transformation.

The Cosmolocal Foundation is executing on eight key initiatives:

  1. Mapping Regenerative Communities — Cataloging eco-villages, circular economy hubs, and governance initiatives worldwide
  2. Web3 Funding — Quadratic funding, DAOs, and Collaborative Finance for local projects
  3. Open Resources — A global knowledge commons of governance models and production blueprints
  4. Education & Advocacy — Engaging policymakers to scale cosmolocal principles
  5. Pilots & Grants — Funding projects that demonstrate scalable solutions
  6. Cosmolocal Certification — Blockchain-verified standards for cosmolocal initiatives
  7. Global Alliances — Bioregional collaboration on governance and mutual aid
  8. Impact Research — Evidence-based scaling through the Cosmolocal Research Institute

Want to learn more or get involved? Reply to this email — we'd love to hear from you.

Best,
The Cosmolocal Foundation Team

\"\n}", + "options": {} + }, + "id": "send-followup-email", + "name": "Send Follow-up Email (Day 3)", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1120, 300] + }, + { + "parameters": { + "amount": 7, + "unit": "days" + }, + "id": "wait-7-days", + "name": "Wait 7 Days", + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [1340, 300] + }, + { + "parameters": { + "method": "POST", + "url": "https://api.resend.com/emails", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "Bearer {{ $env.RESEND_API_KEY }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"from\": \"Cosmolocal Foundation <***REDACTED_EMAIL***>\",\n \"to\": [\"{{ $('Extract Contact Fields').item.json.email }}\"],\n \"subject\": \"Join the Movement — Ways to Participate\",\n \"html\": \"

Hi {{ $('Extract Contact Fields').item.json.firstName }},

We believe in the power of collaboration. Here are ways you can participate in the cosmolocal movement:

Visit cosmolocal.world to explore our work, or simply reply to this email to start a conversation.

Together we can build regenerative economies that serve communities and the planet.

In solidarity,
The Cosmolocal Foundation Team

\"\n}", + "options": {} + }, + "id": "send-engagement-email", + "name": "Send Engagement Email (Day 10)", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1560, 300] + }, + { + "parameters": { + "method": "PATCH", + "url": "=https://crm.cosmolocal.world/api/v1/objects/people/{{ $('Extract Contact Fields').item.json.contactId }}", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "Bearer {{ $env.TWENTY_API_KEY }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"stage\": \"NURTURE_COMPLETE\"\n}", + "options": { + "ignore_ssl_issues": false + } + }, + "id": "update-crm-stage", + "name": "Update CRM — Nurture Complete", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1780, 300] + } + ], + "connections": { + "Webhook — New Contact Created": { + "main": [ + [ + { + "node": "Extract Contact Fields", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract Contact Fields": { + "main": [ + [ + { + "node": "Send Welcome Email (Day 0)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Send Welcome Email (Day 0)": { + "main": [ + [ + { + "node": "Wait 3 Days", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait 3 Days": { + "main": [ + [ + { + "node": "Send Follow-up Email (Day 3)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Send Follow-up Email (Day 3)": { + "main": [ + [ + { + "node": "Wait 7 Days", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait 7 Days": { + "main": [ + [ + { + "node": "Send Engagement Email (Day 10)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Send Engagement Email (Day 10)": { + "main": [ + [ + { + "node": "Update CRM — Nurture Complete", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + }, + "staticData": null, + "tags": [ + { + "name": "cosmolocal" + }, + { + "name": "email" + } + ], + "pinData": {} +} diff --git a/n8n-workflows/03-newsletter-sync.json b/n8n-workflows/03-newsletter-sync.json new file mode 100644 index 0000000..f9ccac3 --- /dev/null +++ b/n8n-workflows/03-newsletter-sync.json @@ -0,0 +1,284 @@ +{ + "name": "Newsletter Sync — CRM to Listmonk", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "triggerAtHour": 6, + "triggerAtMinute": 0 + } + ] + } + }, + "id": "schedule-trigger", + "name": "Daily Sync (6 AM)", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [240, 300] + }, + { + "parameters": { + "method": "GET", + "url": "https://crm.cosmolocal.world/api/v1/objects/people", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "Bearer {{ $env.TWENTY_API_KEY }}" + } + ] + }, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "limit", + "value": "100" + } + ] + }, + "options": {} + }, + "id": "fetch-crm-contacts", + "name": "Fetch CRM Contacts", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [460, 300] + }, + { + "parameters": { + "fieldToSplitOut": "data.people", + "options": {} + }, + "id": "split-contacts", + "name": "Split Into Items", + "type": "n8n-nodes-base.splitOut", + "typeVersion": 1, + "position": [680, 300] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "has-email", + "leftValue": "={{ $json.emails?.primaryEmail }}", + "rightValue": "", + "operator": { + "type": "string", + "operation": "isNotEmpty" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "filter-with-email", + "name": "Filter — Has Email", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [900, 300] + }, + { + "parameters": { + "method": "GET", + "url": "=http://listmonk-cosmolocal:9000/api/subscribers", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "Basic {{ Buffer.from('admin:***REDACTED_LISTMONK_PASS***').toString('base64') }}" + } + ] + }, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "query", + "value": "=subscribers.email = '{{ $json.emails.primaryEmail }}'" + }, + { + "name": "page", + "value": "1" + }, + { + "name": "per_page", + "value": "1" + } + ] + }, + "options": {} + }, + "id": "check-listmonk-exists", + "name": "Check If Subscriber Exists", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1120, 240] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "not-exists", + "leftValue": "={{ $json.data?.total }}", + "rightValue": 0, + "operator": { + "type": "number", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "if-new-subscriber", + "name": "If New Subscriber", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [1340, 240] + }, + { + "parameters": { + "method": "POST", + "url": "http://listmonk-cosmolocal:9000/api/subscribers", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "Basic {{ Buffer.from('admin:***REDACTED_LISTMONK_PASS***').toString('base64') }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"email\": \"{{ $('Filter — Has Email').item.json.emails.primaryEmail }}\",\n \"name\": \"{{ $('Filter — Has Email').item.json.name?.firstName || '' }} {{ $('Filter — Has Email').item.json.name?.lastName || '' }}\",\n \"status\": \"enabled\",\n \"lists\": [1],\n \"preconfirm_subscriptions\": true,\n \"attribs\": {\n \"source\": \"twenty-crm-sync\",\n \"crm_id\": \"{{ $('Filter — Has Email').item.json.id }}\"\n }\n}", + "options": {} + }, + "id": "create-subscriber", + "name": "Create Listmonk Subscriber", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1560, 180] + }, + { + "parameters": {}, + "id": "no-op-exists", + "name": "Already Subscribed — Skip", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1560, 360] + } + ], + "connections": { + "Daily Sync (6 AM)": { + "main": [ + [ + { + "node": "Fetch CRM Contacts", + "type": "main", + "index": 0 + } + ] + ] + }, + "Fetch CRM Contacts": { + "main": [ + [ + { + "node": "Split Into Items", + "type": "main", + "index": 0 + } + ] + ] + }, + "Split Into Items": { + "main": [ + [ + { + "node": "Filter — Has Email", + "type": "main", + "index": 0 + } + ] + ] + }, + "Filter — Has Email": { + "main": [ + [ + { + "node": "Check If Subscriber Exists", + "type": "main", + "index": 0 + } + ], + [] + ] + }, + "Check If Subscriber Exists": { + "main": [ + [ + { + "node": "If New Subscriber", + "type": "main", + "index": 0 + } + ] + ] + }, + "If New Subscriber": { + "main": [ + [ + { + "node": "Create Listmonk Subscriber", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Already Subscribed — Skip", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + }, + "staticData": null, + "tags": [ + { + "name": "cosmolocal" + }, + { + "name": "newsletter" + } + ], + "pinData": {} +} diff --git a/n8n-workflows/04-follow-up-reminders.json b/n8n-workflows/04-follow-up-reminders.json new file mode 100644 index 0000000..5090a95 --- /dev/null +++ b/n8n-workflows/04-follow-up-reminders.json @@ -0,0 +1,237 @@ +{ + "name": "Follow-up Reminders — Stale Contacts", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "triggerAtHour": 9, + "triggerAtMinute": 0, + "triggerAtDay": 1 + } + ] + } + }, + "id": "weekly-schedule", + "name": "Weekly Check (Monday 9 AM)", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [240, 300] + }, + { + "parameters": { + "method": "GET", + "url": "https://crm.cosmolocal.world/api/v1/objects/people", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "Bearer {{ $env.TWENTY_API_KEY }}" + } + ] + }, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "limit", + "value": "100" + } + ] + }, + "options": {} + }, + "id": "fetch-all-contacts", + "name": "Fetch All Contacts", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [460, 300] + }, + { + "parameters": { + "fieldToSplitOut": "data.people", + "options": {} + }, + "id": "split-contacts", + "name": "Split Into Items", + "type": "n8n-nodes-base.splitOut", + "typeVersion": 1, + "position": [680, 300] + }, + { + "parameters": { + "jsCode": "const items = $input.all();\nconst staleItems = [];\nconst now = new Date();\nconst STALE_DAYS = 14;\n\nfor (const item of items) {\n const updatedAt = new Date(item.json.updatedAt);\n const daysSinceUpdate = Math.floor((now - updatedAt) / (1000 * 60 * 60 * 24));\n \n if (daysSinceUpdate >= STALE_DAYS) {\n staleItems.push({\n json: {\n ...item.json,\n daysSinceUpdate,\n email: item.json.emails?.primaryEmail || 'N/A',\n fullName: `${item.json.name?.firstName || ''} ${item.json.name?.lastName || ''}`.trim() || 'Unknown'\n }\n });\n }\n}\n\nreturn staleItems.length > 0 ? staleItems : [{ json: { noStaleContacts: true } }];" + }, + "id": "filter-stale", + "name": "Filter Stale Contacts (14+ days)", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [900, 300] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "has-stale", + "leftValue": "={{ $json.noStaleContacts }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "notTrue" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "if-has-stale", + "name": "Any Stale Contacts?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [1120, 300] + }, + { + "parameters": { + "jsCode": "const items = $input.all();\nconst contactList = items.map(item => \n `- **${item.json.fullName}** (${item.json.email}) — ${item.json.daysSinceUpdate} days since last update`\n).join('\\n');\n\nconst count = items.length;\n\nreturn [{\n json: {\n subject: `[Cosmolocal CRM] ${count} contact${count !== 1 ? 's' : ''} need${count === 1 ? 's' : ''} follow-up`,\n htmlBody: `

Stale Contact Report

The following ${count} contact${count !== 1 ? 's have' : ' has'} not been updated in 14+ days:

${items.map(i => ``).join('')}
NameEmailDays Stale
${i.json.fullName}${i.json.email}${i.json.daysSinceUpdate}

Open CRM to follow up.

Automated by n8n — automate.cosmolocal.world

`,\n count\n }\n}];" + }, + "id": "build-report", + "name": "Build Report Email", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1340, 240] + }, + { + "parameters": { + "method": "POST", + "url": "https://api.resend.com/emails", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "Bearer {{ $env.RESEND_API_KEY }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"from\": \"CRM Bot \",\n \"to\": [\"***REDACTED_EMAIL***\"],\n \"subject\": \"{{ $json.subject }}\",\n \"html\": \"{{ $json.htmlBody }}\"\n}", + "options": {} + }, + "id": "send-reminder-email", + "name": "Send Reminder to Team", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1560, 240] + }, + { + "parameters": {}, + "id": "no-stale-contacts", + "name": "No Stale Contacts — Done", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1340, 420] + } + ], + "connections": { + "Weekly Check (Monday 9 AM)": { + "main": [ + [ + { + "node": "Fetch All Contacts", + "type": "main", + "index": 0 + } + ] + ] + }, + "Fetch All Contacts": { + "main": [ + [ + { + "node": "Split Into Items", + "type": "main", + "index": 0 + } + ] + ] + }, + "Split Into Items": { + "main": [ + [ + { + "node": "Filter Stale Contacts (14+ days)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Filter Stale Contacts (14+ days)": { + "main": [ + [ + { + "node": "Any Stale Contacts?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Any Stale Contacts?": { + "main": [ + [ + { + "node": "Build Report Email", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "No Stale Contacts — Done", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Report Email": { + "main": [ + [ + { + "node": "Send Reminder to Team", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + }, + "staticData": null, + "tags": [ + { + "name": "cosmolocal" + }, + { + "name": "crm" + } + ], + "pinData": {} +} diff --git a/n8n-workflows/05-webhook-events.json b/n8n-workflows/05-webhook-events.json new file mode 100644 index 0000000..79be9bc --- /dev/null +++ b/n8n-workflows/05-webhook-events.json @@ -0,0 +1,347 @@ +{ + "name": "Webhook Events — Gitea/GitHub to CRM", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "git-events", + "options": {} + }, + "id": "webhook-git", + "name": "Webhook — Git Events", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [240, 300], + "webhookId": "git-events" + }, + { + "parameters": { + "jsCode": "const body = $input.first().json.body;\nconst headers = $input.first().json.headers;\n\n// Detect source (Gitea vs GitHub)\nconst isGitea = headers['x-gitea-event'] !== undefined;\nconst isGitHub = headers['x-github-event'] !== undefined;\n\nconst eventType = isGitea \n ? headers['x-gitea-event'] \n : headers['x-github-event'] || 'unknown';\n\nlet result = {\n source: isGitea ? 'gitea' : isGitHub ? 'github' : 'unknown',\n eventType,\n repo: body.repository?.full_name || body.repository?.name || 'unknown',\n action: body.action || 'push',\n sender: body.sender?.login || body.pusher?.name || 'unknown',\n senderEmail: body.sender?.email || body.pusher?.email || '',\n url: '',\n title: '',\n description: ''\n};\n\nswitch (eventType) {\n case 'push':\n const commits = body.commits || [];\n result.title = `Push: ${commits.length} commit(s) to ${result.repo}`;\n result.description = commits.map(c => `- ${c.message}`).join('\\n');\n result.url = body.compare_url || body.compare || '';\n break;\n case 'issues':\n result.title = `Issue ${body.action}: ${body.issue?.title}`;\n result.description = body.issue?.body || '';\n result.url = body.issue?.html_url || '';\n break;\n case 'pull_request':\n result.title = `PR ${body.action}: ${body.pull_request?.title}`;\n result.description = body.pull_request?.body || '';\n result.url = body.pull_request?.html_url || '';\n break;\n case 'create':\n result.title = `Created ${body.ref_type}: ${body.ref}`;\n result.description = `New ${body.ref_type} created in ${result.repo}`;\n break;\n case 'star':\n case 'watch':\n result.title = `${result.sender} starred ${result.repo}`;\n result.description = `Repository now has ${body.repository?.stars_count || body.repository?.stargazers_count || '?'} stars`;\n break;\n default:\n result.title = `${eventType} event on ${result.repo}`;\n result.description = JSON.stringify(body).substring(0, 500);\n}\n\nreturn [{ json: result }];" + }, + "id": "parse-event", + "name": "Parse Git Event", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [460, 300] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "is-significant", + "leftValue": "={{ $json.eventType }}", + "rightValue": "push,issues,pull_request,star", + "operator": { + "type": "string", + "operation": "contains" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "filter-significant", + "name": "Filter Significant Events", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [680, 300] + }, + { + "parameters": { + "method": "POST", + "url": "https://crm.cosmolocal.world/api/v1/objects/notes", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "Bearer {{ $env.TWENTY_API_KEY }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"title\": \"[{{ $json.source }}] {{ $json.title }}\",\n \"body\": \"**Event**: {{ $json.eventType }}\\n**Repo**: {{ $json.repo }}\\n**By**: {{ $json.sender }}\\n\\n{{ $json.description }}\\n\\n{{ $json.url ? '[View on ' + $json.source + '](' + $json.url + ')' : '' }}\"\n}", + "options": {} + }, + "id": "log-to-crm", + "name": "Log Activity to CRM", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [900, 240] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "has-email", + "leftValue": "={{ $('Parse Git Event').item.json.senderEmail }}", + "rightValue": "", + "operator": { + "type": "string", + "operation": "isNotEmpty" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "has-sender-email", + "name": "Sender Has Email?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [1120, 240] + }, + { + "parameters": { + "method": "GET", + "url": "https://crm.cosmolocal.world/api/v1/objects/people", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "Bearer {{ $env.TWENTY_API_KEY }}" + } + ] + }, + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "filter", + "value": "={\"emails\":{\"primaryEmail\":{\"eq\":\"{{ $('Parse Git Event').item.json.senderEmail }}\"}}}" + } + ] + }, + "options": {} + }, + "id": "find-contact", + "name": "Find Contact by Email", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1340, 180] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "contact-found", + "leftValue": "={{ $json.data?.people?.length }}", + "rightValue": 0, + "operator": { + "type": "number", + "operation": "gt" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "contact-exists", + "name": "Contact Found?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [1560, 180] + }, + { + "parameters": { + "method": "PATCH", + "url": "=https://crm.cosmolocal.world/api/v1/objects/people/{{ $json.data.people[0].id }}", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "Bearer {{ $env.TWENTY_API_KEY }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"updatedAt\": \"{{ new Date().toISOString() }}\"\n}", + "options": {} + }, + "id": "touch-contact", + "name": "Touch Contact — Update Timestamp", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1780, 120] + }, + { + "parameters": { + "method": "POST", + "url": "https://crm.cosmolocal.world/api/v1/objects/people", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "Bearer {{ $env.TWENTY_API_KEY }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"name\": {\n \"firstName\": \"{{ $('Parse Git Event').item.json.sender }}\",\n \"lastName\": \"\"\n },\n \"emails\": {\n \"primaryEmail\": \"{{ $('Parse Git Event').item.json.senderEmail }}\"\n }\n}", + "options": {} + }, + "id": "create-contributor-contact", + "name": "Create Contributor Contact", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1780, 280] + }, + { + "parameters": {}, + "id": "no-op-skip", + "name": "Skip — Not Significant", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [900, 420] + } + ], + "connections": { + "Webhook — Git Events": { + "main": [ + [ + { + "node": "Parse Git Event", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse Git Event": { + "main": [ + [ + { + "node": "Filter Significant Events", + "type": "main", + "index": 0 + } + ] + ] + }, + "Filter Significant Events": { + "main": [ + [ + { + "node": "Log Activity to CRM", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Skip — Not Significant", + "type": "main", + "index": 0 + } + ] + ] + }, + "Log Activity to CRM": { + "main": [ + [ + { + "node": "Sender Has Email?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Sender Has Email?": { + "main": [ + [ + { + "node": "Find Contact by Email", + "type": "main", + "index": 0 + } + ], + [] + ] + }, + "Find Contact by Email": { + "main": [ + [ + { + "node": "Contact Found?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Contact Found?": { + "main": [ + [ + { + "node": "Touch Contact — Update Timestamp", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Create Contributor Contact", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + }, + "staticData": null, + "tags": [ + { + "name": "cosmolocal" + }, + { + "name": "git" + }, + { + "name": "crm" + } + ], + "pinData": {} +} diff --git a/n8n-workflows/README.md b/n8n-workflows/README.md new file mode 100644 index 0000000..81515bb --- /dev/null +++ b/n8n-workflows/README.md @@ -0,0 +1,64 @@ +# Cosmolocal n8n Workflows + +Import these JSON files into [automate.cosmolocal.world](https://automate.cosmolocal.world) via **Settings > Import Workflow**. + +## Setup Requirements + +Before activating workflows, configure these environment variables in n8n (**Settings > Variables**): + +| Variable | Description | Where to find | +|----------|-------------|---------------| +| `TWENTY_API_KEY` | Twenty CRM API key | crm.cosmolocal.world > Settings > API Keys | +| `RESEND_API_KEY` | Resend email API key | `ssh netcup "cat ~/.resend_credentials"` | + +The Listmonk credentials are hardcoded for internal Docker network access (no external exposure). + +## Workflows + +### 01 — Contact Intake (Form to CRM) +**Trigger:** Webhook POST to `/webhook/contact-intake` +**Flow:** Validate input > Create contact in Twenty CRM > Add note with message > Respond + +Use this webhook URL in your website contact form: +``` +https://automate.cosmolocal.world/webhook/contact-intake +``` + +POST body: +```json +{ + "name": "Jane Doe", + "email": "jane@example.com", + "phone": "+1234567890", + "message": "Interested in collaborating" +} +``` + +### 02 — Lead Nurturing (Welcome Email Sequence) +**Trigger:** Webhook POST to `/webhook/new-contact-created` +**Flow:** Day 0: Welcome email > Day 3: Strategic priorities > Day 10: Ways to participate > Update CRM stage + +Chain this from workflow 01 or call it when a new contact is created in the CRM. + +### 03 — Newsletter Sync (CRM to Listmonk) +**Trigger:** Daily at 6:00 AM +**Flow:** Fetch CRM contacts > Check if already in Listmonk > Create new subscribers + +Syncs all CRM contacts with email addresses to Listmonk list #1. Adjust the list ID if needed. + +### 04 — Follow-up Reminders (Stale Contacts) +**Trigger:** Weekly on Monday at 9:00 AM +**Flow:** Fetch contacts > Filter those not updated in 14+ days > Email report to team + +Sends an HTML table report to `***REDACTED_EMAIL***` with stale contacts and a link to the CRM. + +### 05 — Webhook Events (Git to CRM) +**Trigger:** Webhook POST to `/webhook/git-events` +**Flow:** Parse Gitea/GitHub event > Log as CRM note > Find or create contributor contact + +Add this webhook URL to Gitea/GitHub repos: +``` +https://automate.cosmolocal.world/webhook/git-events +``` + +Supports: push, issues, pull_request, star/watch events. Auto-detects Gitea vs GitHub from headers.