Compare commits
7 Commits
0a63bbe9f7
...
551ae0d217
| Author | SHA1 | Date |
|---|---|---|
|
|
551ae0d217 | |
|
|
f59b3efb81 | |
|
|
e929c4563e | |
|
|
dc55322103 | |
|
|
c257f830c9 | |
|
|
7c01037970 | |
|
|
6b578d2c7f |
|
|
@ -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:
|
||||
- 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}
|
||||
- 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}
|
||||
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:
|
||||
traefik-public:
|
||||
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