Add 5 n8n CRM automation workflows

- 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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-07 14:20:28 +00:00
parent acd874924f
commit 5b87103fdb
6 changed files with 1417 additions and 0 deletions

View File

@ -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": {}
}

View File

@ -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\": \"<h2>Welcome, {{ $json.firstName }}!</h2><p>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.</p><p><strong>\\\"What is heavy should be local, and what is light should be global and shared.\\\"</strong></p><p>Here's what we're working on:</p><ul><li><strong>Decentralized Governance</strong> — Transparent, community-led decision-making</li><li><strong>Open Knowledge Commons</strong> — Sharing sustainable production methods globally</li><li><strong>Commons-Compatible Capital</strong> — Financing transformative local projects</li><li><strong>Cosmolocal Coordination</strong> — Connecting local efforts into a global network</li></ul><p>We'll be in touch with updates on our progress and opportunities to get involved.</p><p>Warm regards,<br/>The Cosmolocal Foundation Team</p><p style='color:#78716c;font-size:12px;'>cosmolocal.world</p>\"\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\": \"<h2>Hi {{ $('Extract Contact Fields').item.json.firstName }},</h2><p>We wanted to share more about our strategic approach to systemic transformation.</p><p>The Cosmolocal Foundation is executing on eight key initiatives:</p><ol><li><strong>Mapping Regenerative Communities</strong> — Cataloging eco-villages, circular economy hubs, and governance initiatives worldwide</li><li><strong>Web3 Funding</strong> — Quadratic funding, DAOs, and Collaborative Finance for local projects</li><li><strong>Open Resources</strong> — A global knowledge commons of governance models and production blueprints</li><li><strong>Education & Advocacy</strong> — Engaging policymakers to scale cosmolocal principles</li><li><strong>Pilots & Grants</strong> — Funding projects that demonstrate scalable solutions</li><li><strong>Cosmolocal Certification</strong> — Blockchain-verified standards for cosmolocal initiatives</li><li><strong>Global Alliances</strong> — Bioregional collaboration on governance and mutual aid</li><li><strong>Impact Research</strong> — Evidence-based scaling through the Cosmolocal Research Institute</li></ol><p>Want to learn more or get involved? Reply to this email — we'd love to hear from you.</p><p>Best,<br/>The Cosmolocal Foundation Team</p>\"\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\": \"<h2>Hi {{ $('Extract Contact Fields').item.json.firstName }},</h2><p>We believe in the power of collaboration. Here are ways you can participate in the cosmolocal movement:</p><ul><li><strong>Subscribe to our newsletter</strong> — Stay updated on projects, pilots, and governance experiments</li><li><strong>Join a working group</strong> — Contribute your expertise to research, technology, or community building</li><li><strong>Support a pilot project</strong> — Help fund or participate in cosmolocal demonstrations</li><li><strong>Spread the word</strong> — Share our vision with your network</li></ul><p>Visit <a href='https://cosmolocal.world'>cosmolocal.world</a> to explore our work, or simply reply to this email to start a conversation.</p><p>Together we can build regenerative economies that serve communities and the planet.</p><p>In solidarity,<br/>The Cosmolocal Foundation Team</p>\"\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": {}
}

View File

@ -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": {}
}

View File

@ -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: `<h2>Stale Contact Report</h2><p>The following ${count} contact${count !== 1 ? 's have' : ' has'} not been updated in 14+ days:</p><table style=\"border-collapse:collapse;width:100%\"><tr style=\"background:#f5f5f4\"><th style=\"padding:8px;text-align:left;border:1px solid #e7e5e4\">Name</th><th style=\"padding:8px;text-align:left;border:1px solid #e7e5e4\">Email</th><th style=\"padding:8px;text-align:left;border:1px solid #e7e5e4\">Days Stale</th></tr>${items.map(i => `<tr><td style=\"padding:8px;border:1px solid #e7e5e4\">${i.json.fullName}</td><td style=\"padding:8px;border:1px solid #e7e5e4\">${i.json.email}</td><td style=\"padding:8px;border:1px solid #e7e5e4\">${i.json.daysSinceUpdate}</td></tr>`).join('')}</table><p style=\"margin-top:16px\"><a href=\"https://crm.cosmolocal.world\">Open CRM</a> to follow up.</p><p style=\"color:#78716c;font-size:12px\">Automated by n8n — automate.cosmolocal.world</p>`,\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 <automate@cosmolocal.world>\",\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": {}
}

View File

@ -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": {}
}

64
n8n-workflows/README.md Normal file
View File

@ -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.