Twenty CRM orchestrates 20.tn school outreach; platform-bus on dardev-vps executes sends, syncs Listmonk engagement, scans the reply inbox, and writes timeline Notes on every stage transition. CRM owns who gets mail and when humans approve. The bus is a thin executor—not a bulk brain that blasts thousands of rows from a CSV.
This guide is for operators and engineers wiring workflows. Sales staff should read 20tn-outreach-one-school-at-a-time instead. For mail topology see inside-unified-mailer-stack; for consent gates see crm-driven-product-lists; for company honesty see deployed-realities-explained.
Architecture layers
- Twenty workflows — triggers, filters, human approval, scheduled sync, task creation
- Person record — outreachStatus, outreachApproved, outreachPersonalLine, outreachCustomSubject
- Timeline Notes — Queued, Sent, Opened, Clicked, Replied; one Note per transition written by bus
- Platform-bus — send-one, sync-engagement, sync-replies on mail.dardev.net
- Listmonk — tracked transactional and campaign delivery only; not cold-import audience store
Pipeline stages
A Person starts at NOT_SENT. Preview workflow with dryRun queues a timeline note without delivery. Approved live send moves to SENT via send-one. Scheduled sync-engagement polls Listmonk for opens and clicks, updating OPENED and CLICKED. sync-replies scans the product inbox for inbound mail and sets REPLIED. Listmonk unsubscribe maps to OPT_OUT. Validation failures land in ERROR with Outreach last error on the Person.
Every transition creates a visible Note on the Person timeline so sales and ops audit the same story without reading server logs.
Customization surfaces
Per-person CRM fields override template defaults at send time. outreachCustomSubject replaces the default subject. outreachPersonalLine injects the human paragraph after the greeting. outreachApproved must be true when the workflow passes requireApproved—staff toggle after preview.
Run scripts/twenty/setup_20tn_crm_fields.py once per workspace to create outreach and school metadata fields. Workflow HTTP actions POST JSON to platform-bus with personId, phase (intro or followup), dryRun boolean, requireApproved, and optional templateVars such as videoUrl, preheader, and personalLine merged from trigger fields.
Global defaults—sender identity, default subject, video URL—live in VPS environment configuration merged under workflow overrides. HTML templates live in listmonk/templates/20tn-intro-outreach-body.html wrapped by 20tn-campaign.html for tracking pixels and link rewrite.
Workflow catalog
Generate workflow payloads from the repo with scripts/20tn-outreach/bootstrap_workflows.py --print-v2. Import into Twenty UI or MCP. Names use the 20tn — prefix for production.
- 20tn — preview intro — Manual on Person — POST send-one with dryRun true
- 20tn — send intro — Manual on Person — filter outreachApproved — live send-one
- 20tn — request review — Manual — Task plus outreachApproved false
- 20tn — follow-up — Person updated when outreachStatus SENT — three-day delay — phase followup
- 20tn — opened task — filter OPENED — linked Task on Person and Company
- 20tn — clicked task — filter CLICKED — hot-lead task title
- 20tn — replied task — filter REPLIED — reply follow-up task
- 20tn — sync engagement — schedule every fifteen minutes — POST sync-engagement
- 20tn — sync replies — schedule every thirty minutes — POST sync-replies
Chained human loop: request review → staff edits outreachPersonalLine → sets outreachApproved true → send intro. Engagement workflows create Tasks instead of auto-sending the next mail—sales calls humans who opened or clicked.
Platform-bus executor routes
Primary production paths on mail.dardev.net (platform-bus port 3100 behind nginx):
- POST /api/20tn/outreach/send-one — tracked Listmonk send for one Person; syncs subscriber row first
- POST /api/20tn/outreach/sync-engagement — Listmonk open and click events to CRM status fields
- POST /api/20tn/outreach/sync-replies — IMAP scan of outreach inbox to REPLIED
- POST /api/sync/20tn-outreach — sync one person to Listmonk list (send-one invokes automatically)
- POST /api/20tn/outreach/poc — legacy direct SMTP chain; prefer send-one
Twenty workflow HTTP steps call these URLs with JSON bodies. Authentication uses a shared secret header configured on the VPS—store it in Twenty workflow credentials or secret manager, never in this article or public git. Rotate if exposed.
Example send-one body shape (no live credentials): personId UUID, phase intro, dryRun false, requireApproved true, optional templateVars object. Ops test with dryRun true before enabling live cohorts.
Human-in-the-loop patterns
- Approve before send — workflow filter outreachApproved equals true
- Preview first — dryRun true shows Queued note with subject and recipient
- Follow-up gate — three-day delay; cancel run from See Runs if staff already called
- Engagement tasks — OPENED, CLICKED, REPLIED create Tasks instead of next automated mail
- No batch blast — iterate 20tn ready view with manual workflow per row
Rollout and live-send gates
Early cohorts run preview and dry-run end to end on staging Persons. Live delivery to real school inboxes requires operations to enable production sends after template, DNS, and warm-up checks pass. The bus enforces allow-live policy server-side—staff see a clear blocked message rather than silent failure. Do not document bypass paths in runbooks; request ops enablement per rollout phase.
Test cohort JSON under data/20tn-outreach-test-cohort.json lists Person IDs for scripted e2e. Deactivate legacy workflows whose names start with 20tn PoC before training sales on 20tn — names.
Setup checklist
- python scripts/twenty/setup_20tn_crm_fields.py — outreach and customization fields
- Deploy platform-bus — scripts/ops/deploy/rebuild_platform_bus_vps.py
- Create workflows from bootstrap_workflows.py --print-v2 payloads
- Activate schedule workflows for engagement and replies
- Validate on test cohort before sales live intros
- Confirm Listmonk product list and templates deployed on dardev-vps
Observability and failure modes
Person.Outreach last error stores bus validation messages—missing approval, blocked live send, template render failure. Timeline Notes supplement audit. If sync-engagement stalls, check Listmonk campaign linkage and bus logs on dardev-vps. If replies never mark REPLIED, verify IMAP credentials and mailbox filters for the product outreach address.
Twenty task workflows that omit Person linkage were patched via ops scripts—if tasks appear orphaned, re-run the task workflow patch documented in scripts/20tn-outreach/README.md.
Warm-up discipline applies: company mail infrastructure targets on the order of ten thousand tracked messages per month across products. 20.tn intros share that capacity with other DarDev outbound programs—another reason workflows stay per-Person instead of firing one job across an entire imported view.
Relation to prospect import
Schools enter CRM from ecoles.com.tn scraping described in ecoles-scrape-to-crm-pipeline. Import sets consentEmail UNKNOWN and targetSolution 20.tn. Outreach workflows assume that ethical baseline—send-one does not subscribe cold schools to marketing lists. Opt-in later follows crm-driven-product-lists patterns when a subscribe form exists.
Product narrative for directors is in 20tn-vision-tunisia-schools. Sales motion without server detail is in 20tn-outreach-one-school-at-a-time.
Why not one bulk POST for all schools?
Deliverability, consent, and personalization. Each Person carries approval and a personal line. Bulk send bypasses human gates and duplicates the mistakes crm-driven-product-lists warns against.
Where does Listmonk fit if CRM orchestrates?
Listmonk sends tracked mail and records opens and clicks. CRM holds relationship state and tasks. Bus syncs events between them on a schedule.
Can workflows call send-one without approval?
Technically yes if requireApproved is false—do not do that in production. Preview and send intro workflows use requireApproved true for live phase.
How do I rotate the bus secret?
Update VPS env and Twenty workflow HTTP headers together. Never commit secrets to git or news articles. Test dryRun after rotation.
What changed from the PoC workflows?
PoC chains used direct SMTP experiments and retired names. Production uses send-one, Listmonk tracking, scheduled sync, and task-based sales follow-up—documented here and in scripts/20tn-outreach/README.md.



