Compare commits
6 Commits
551ae0d217
...
0a63bbe9f7
| Author | SHA1 | Date |
|---|---|---|
|
|
0a63bbe9f7 | |
|
|
c8b9388c63 | |
|
|
df641f6208 | |
|
|
5ea319e03c | |
|
|
8d55469c4e | |
|
|
b5566c53dd |
|
|
@ -0,0 +1,5 @@
|
||||||
|
project_name: CosmoLocal Website
|
||||||
|
project_id: cosmolocal-website
|
||||||
|
description: CosmoLocal World website and related infrastructure
|
||||||
|
created: '2026-02-09'
|
||||||
|
integration_mode: mcp
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
---
|
||||||
|
id: task-1
|
||||||
|
title: Set up CosmoLocal email, docs, and newsletter infrastructure
|
||||||
|
status: Done
|
||||||
|
assignee: ['@claude']
|
||||||
|
created_date: '2026-02-09 12:00'
|
||||||
|
updated_date: '2026-02-09 21:30'
|
||||||
|
labels: [infrastructure, email, docs, newsletter]
|
||||||
|
dependencies: []
|
||||||
|
priority: high
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Set up CosmoLocal World infrastructure: Mailcow SMTP for cosmolocal.world, Docmost workspace at docs.cosmolocal.world, Listmonk newsletter list with per-list RBAC for Bryan, and email authentication (DKIM, SPF, DMARC).
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 Configure Mailcow SMTP for cosmolocal.world domain
|
||||||
|
- [x] #2 Set up DNS records (SPF, DKIM, DMARC) for cosmolocal.world
|
||||||
|
- [x] #3 Create noreply@cosmolocal.world mailbox with newsletter@ alias
|
||||||
|
- [x] #4 Deploy Docmost at docs.cosmolocal.world (separate workspace, shared infra)
|
||||||
|
- [x] #5 Configure SMTP for Docmost CosmoLocal instance
|
||||||
|
- [x] #6 Create CosmoLocal World list in Listmonk
|
||||||
|
- [x] #7 Set up Bryan as editor with per-list RBAC (CosmoLocal list only)
|
||||||
|
- [x] #8 Set up Google Postmaster Tools for cosmolocal.world
|
||||||
|
- [x] #9 Configure Traefik websecure + Let's Encrypt for docs.cosmolocal.world
|
||||||
|
- [x] #10 Invite Bryan to CosmoLocal Docmost workspace
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
### Mailcow (cosmolocal.world)
|
||||||
|
- Mailbox: noreply@cosmolocal.world
|
||||||
|
- Alias: newsletter@cosmolocal.world → noreply@cosmolocal.world (sender_allowed=1)
|
||||||
|
- SMTP: mx.jeffemmett.com:465 (TLS)
|
||||||
|
- DNS: SPF, DKIM (2048-bit), DMARC all configured on Cloudflare
|
||||||
|
- Google Postmaster Tools verified
|
||||||
|
|
||||||
|
### Docmost (docs.cosmolocal.world)
|
||||||
|
- Separate Docmost app container (docmost-cl) sharing Postgres + Redis with docs.jeffemmett.com
|
||||||
|
- Database: docmost_cosmolocal (in shared docmost-db Postgres)
|
||||||
|
- Redis: db 1 (shared docmost-redis)
|
||||||
|
- DNS: proxied A record → 159.195.32.209
|
||||||
|
- SSL: Traefik websecure entrypoint + Let's Encrypt cert
|
||||||
|
- SMTP: noreply@cosmolocal.world via Mailcow
|
||||||
|
- Location: /opt/apps/docmost/docker-compose.yml (single compose file)
|
||||||
|
|
||||||
|
### Listmonk (newsletter.cosmolocal.world)
|
||||||
|
- CosmoLocal World list created (id=21, public, single opt-in)
|
||||||
|
- SMTP server "cosmolocal.world" configured in Listmonk settings
|
||||||
|
- Bryan's account: bryan / CosmoLocal-e2dc5eec
|
||||||
|
- User role: Editor (campaigns, subscribers, templates, media - no admin)
|
||||||
|
- List role: CosmoLocal Editor (scoped to CosmoLocal World list only)
|
||||||
|
- Cannot see other lists, settings, users, or roles
|
||||||
|
|
||||||
|
### Bryan's Access Summary
|
||||||
|
| Service | URL | Username | Role |
|
||||||
|
|---------|-----|----------|------|
|
||||||
|
| Listmonk | newsletter.jeffemmett.com/admin | bryan | Editor (CosmoLocal list only) |
|
||||||
|
| Docmost | docs.cosmolocal.world | bryan@cosmolocal.world | Member (invited) |
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
@ -15,6 +15,72 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- traefik-public
|
- traefik-public
|
||||||
|
|
||||||
|
n8n-cosmolocal:
|
||||||
|
image: n8nio/n8n:latest
|
||||||
|
container_name: n8n-cosmolocal
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- N8N_HOST=automate.cosmolocal.world
|
||||||
|
- N8N_PROTOCOL=https
|
||||||
|
- WEBHOOK_URL=https://automate.cosmolocal.world/
|
||||||
|
- GENERIC_TIMEZONE=Europe/Brussels
|
||||||
|
- DB_TYPE=postgresdb
|
||||||
|
- DB_POSTGRESDB_HOST=n8n-cosmolocal-db
|
||||||
|
- DB_POSTGRESDB_PORT=5432
|
||||||
|
- DB_POSTGRESDB_DATABASE=n8n
|
||||||
|
- DB_POSTGRESDB_USER=n8n
|
||||||
|
- DB_POSTGRESDB_PASSWORD=${N8N_DB_PASSWORD:-cosmolocal-n8n-2026}
|
||||||
|
- TWENTY_API_KEY=${TWENTY_API_KEY}
|
||||||
|
- LISTMONK_CREDENTIALS=${LISTMONK_CREDENTIALS}
|
||||||
|
- N8N_SMTP_HOST=mx.jeffemmett.com
|
||||||
|
- N8N_SMTP_PORT=587
|
||||||
|
- N8N_SMTP_USER=${SMTP_USER}
|
||||||
|
- N8N_SMTP_PASS=${SMTP_PASS}
|
||||||
|
- N8N_SMTP_SENDER=${SMTP_SENDER:-hello@cosmolocal.world}
|
||||||
|
- N8N_EMAIL_MODE=smtp
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
volumes:
|
||||||
|
- n8n-cosmolocal-data:/home/node/.n8n
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.n8n-cosmolocal.rule=Host(`automate.cosmolocal.world`)"
|
||||||
|
- "traefik.http.routers.n8n-cosmolocal.entrypoints=web"
|
||||||
|
- "traefik.http.services.n8n-cosmolocal.loadbalancer.server.port=5678"
|
||||||
|
- "traefik.http.routers.n8n-cosmolocal-secure.rule=Host(`automate.cosmolocal.world`)"
|
||||||
|
- "traefik.http.routers.n8n-cosmolocal-secure.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.n8n-cosmolocal-secure.tls=true"
|
||||||
|
- "traefik.http.routers.n8n-cosmolocal-secure.service=n8n-cosmolocal"
|
||||||
|
depends_on:
|
||||||
|
n8n-cosmolocal-db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- traefik-public
|
||||||
|
- cosmolocal-internal
|
||||||
|
|
||||||
|
n8n-cosmolocal-db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: n8n-cosmolocal-db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=n8n
|
||||||
|
- POSTGRES_USER=n8n
|
||||||
|
- POSTGRES_PASSWORD=${N8N_DB_PASSWORD:-cosmolocal-n8n-2026}
|
||||||
|
volumes:
|
||||||
|
- n8n-cosmolocal-db:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U n8n"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- cosmolocal-internal
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
n8n-cosmolocal-data:
|
||||||
|
n8n-cosmolocal-db:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
traefik-public:
|
traefik-public:
|
||||||
external: true
|
external: true
|
||||||
|
cosmolocal-internal:
|
||||||
|
|
|
||||||
|
|
@ -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": {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,257 @@
|
||||||
|
{
|
||||||
|
"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": {
|
||||||
|
"fromEmail": "={{ $env.N8N_SMTP_SENDER }}",
|
||||||
|
"toEmail": "={{ $json.email }}",
|
||||||
|
"subject": "Welcome to the Cosmolocal Foundation",
|
||||||
|
"emailType": "html",
|
||||||
|
"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>",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "send-welcome-email",
|
||||||
|
"name": "Send Welcome Email (Day 0)",
|
||||||
|
"type": "n8n-nodes-base.emailSend",
|
||||||
|
"typeVersion": 2.1,
|
||||||
|
"position": [680, 300],
|
||||||
|
"credentials": {
|
||||||
|
"smtp": {
|
||||||
|
"id": "mailcow-smtp",
|
||||||
|
"name": "Mailcow SMTP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": {
|
||||||
|
"fromEmail": "={{ $env.N8N_SMTP_SENDER }}",
|
||||||
|
"toEmail": "={{ $('Extract Contact Fields').item.json.email }}",
|
||||||
|
"subject": "Our Strategic Priorities — How We're Building Change",
|
||||||
|
"emailType": "html",
|
||||||
|
"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>",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "send-followup-email",
|
||||||
|
"name": "Send Follow-up Email (Day 3)",
|
||||||
|
"type": "n8n-nodes-base.emailSend",
|
||||||
|
"typeVersion": 2.1,
|
||||||
|
"position": [1120, 300],
|
||||||
|
"credentials": {
|
||||||
|
"smtp": {
|
||||||
|
"id": "mailcow-smtp",
|
||||||
|
"name": "Mailcow SMTP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": {
|
||||||
|
"fromEmail": "={{ $env.N8N_SMTP_SENDER }}",
|
||||||
|
"toEmail": "={{ $('Extract Contact Fields').item.json.email }}",
|
||||||
|
"subject": "Join the Movement — Ways to Participate",
|
||||||
|
"emailType": "html",
|
||||||
|
"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>",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "send-engagement-email",
|
||||||
|
"name": "Send Engagement Email (Day 10)",
|
||||||
|
"type": "n8n-nodes-base.emailSend",
|
||||||
|
"typeVersion": 2.1,
|
||||||
|
"position": [1560, 300],
|
||||||
|
"credentials": {
|
||||||
|
"smtp": {
|
||||||
|
"id": "mailcow-smtp",
|
||||||
|
"name": "Mailcow SMTP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
|
@ -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($env.LISTMONK_CREDENTIALS || 'admin:changeme').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($env.LISTMONK_CREDENTIALS || 'admin:changeme').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": {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
{
|
||||||
|
"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 count = items.length;\n\nconst tableRows = 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('');\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>${tableRows}</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": {
|
||||||
|
"fromEmail": "={{ $env.N8N_SMTP_SENDER }}",
|
||||||
|
"toEmail": "hello@cosmolocal.world",
|
||||||
|
"subject": "={{ $json.subject }}",
|
||||||
|
"emailType": "html",
|
||||||
|
"html": "={{ $json.htmlBody }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "send-reminder-email",
|
||||||
|
"name": "Send Reminder to Team",
|
||||||
|
"type": "n8n-nodes-base.emailSend",
|
||||||
|
"typeVersion": 2.1,
|
||||||
|
"position": [1560, 240],
|
||||||
|
"credentials": {
|
||||||
|
"smtp": {
|
||||||
|
"id": "mailcow-smtp",
|
||||||
|
"name": "Mailcow SMTP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
|
@ -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": {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Cosmolocal n8n Workflows
|
||||||
|
|
||||||
|
Import these JSON files into [automate.cosmolocal.world](https://automate.cosmolocal.world) via **Settings > Import Workflow**.
|
||||||
|
|
||||||
|
## Setup Requirements
|
||||||
|
|
||||||
|
API keys are passed as **Docker environment variables** in `docker-compose.yml` (n8n community edition doesn't support Settings > Variables). The workflows access them via `$env.VARIABLE_NAME`.
|
||||||
|
|
||||||
|
| Variable | Description | Where to find |
|
||||||
|
|----------|-------------|---------------|
|
||||||
|
| `TWENTY_API_KEY` | Twenty CRM API key | crm.cosmolocal.world > Settings > API Keys |
|
||||||
|
| `SMTP_USER` | Mailcow SMTP username | e.g. `hello@cosmolocal.world` |
|
||||||
|
| `SMTP_PASS` | Mailcow SMTP password | Mailcow admin panel at mx.jeffemmett.com |
|
||||||
|
| `LISTMONK_CREDENTIALS` | Listmonk `user:pass` | Internal Docker service credentials |
|
||||||
|
|
||||||
|
Email is sent via **Mailcow SMTP** (`mx.jeffemmett.com:587` STARTTLS). After importing workflows, create an SMTP credential in n8n named "Mailcow SMTP" with the host/user/pass above.
|
||||||
|
|
||||||
|
To set keys, create `/opt/websites/cosmolocal-website/.env` on the server and redeploy.
|
||||||
|
|
||||||
|
## 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 `hello@cosmolocal.world` 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.
|
||||||
Loading…
Reference in New Issue