DarDev 20.tn school outreach sends one tracked message per CRM-approved Person through Listmonk transactional APIs—not through bulk campaign blasts. Platform-bus POST /api/20tn/outreach/send-one picks a subscriber on the private product list, renders the French intro template with school-specific placeholders, and records outreachStatus back in Twenty. Opens and link clicks sync to CRM for follow-up tasks without treating cold directors like newsletter subscribers.
This pattern sits between marketing automation and raw SMTP: you get TrackView and TrackLink analytics like a campaign, but volume stays human-gated one school at a time as documented in 20tn-outreach-one-school-at-a-time. Consent and list membership rules come from crm-driven-product-lists; transport and templates live in the Unified Mailer stack in inside-unified-mailer-stack.
Why not a Listmonk campaign for cold outreach
Listmonk campaigns excel at opted-in lists: predictable batch size, uniform send time, and aggregate stats. Cold school prospects imported from ecoles.com.tn arrive with consentEmail UNKNOWN—they belong in CRM for segmentation and approval, not in a scheduled blast. Campaign mode also makes it easy to bypass per-recipient personalization and human preview, which 20.tn workflows require before the first intro leaves the building.
- Campaign — many recipients, one send action, marketing list semantics
- Transactional TX — one recipient per API call, template per message, CRM correlation
- Direct SMTP — no Listmonk tracking; used for internal digest, not 20.tn intros
- Zoho Send Email from Twenty — human 1:1 only; no TrackView sync to outreach fields
Template structure in the repo
Listmonk stores a wrapper and body fragment under listmonk/templates/. File 20tn-campaign.html is the HTML shell: preheader slot, dark header bar with 20.dardev.net wordmark, mobile styles, and a footer block. File 20tn-intro-outreach-body.html holds the French copy—secretariat pain points, bullet list of product outcomes, video CTA, and signature block with sender placeholders.
Go template variables such as {{ .Subscriber.Name }} personalize the greeting when the synced Listmonk subscriber carries a name. Bus-side replacements run before SMTP: __20TN_SCHOOL_NAME__, __20TN_PERSONAL_LINE__ from CRM outreachPersonalLine, __20TN_VIDEO_URL__, __20TN_SENDER_NAME__, and reply contacts. Keeping placeholders in double-underscore form lets platform-bus validate rendering without exposing Listmonk admin credentials to workflow authors.
Tracking hooks belong in the wrapper or body where Listmonk expects them: {{ TrackView }} for open pixels and tracked links on https://20.dardev.net CTAs. Follow-up phases can swap body fragments—20tn-followup-outreach-body.html—while reusing the same campaign shell for visual consistency.
CRM to Listmonk sync gate
Before send-one can fire, POST /api/sync/20tn-outreach promotes eligible Persons to Listmonk list product:20tn:marketing—a private list with preconfirm_subscriptions true. Sync filters include targetSolution OPT20TN, regionMarket TUNISIA, schoolOwnership PRIVE on the related company, and consentEmail in UNKNOWN or OPT_IN never OPT_OUT. Dry run query ?dry_run=1 returns counts of candidates, skipped consent, and already-subscribed rows without mutating Listmonk.
Sync does not mean blast. It creates a tracked subscriber row tied to CRM email so transactional APIs have a recipient id. Actual SMTP waits for workflow approval or manual send-one with dryRun false. This mirrors DarDev Services outreach: CRM owns truth; Listmonk is a delivery and analytics edge.
send-one request flow
- Twenty workflow or Ops action supplies personId and phase intro or followup
- Platform-bus validates OPT20TN, private school, email present, outreach gates
- Optional delayMs spaces sends for deliverability warm-up
- Bus renders template with CRM and env placeholders
- Listmonk transactional or mail-jobs single-message path sends with tracking
- CRM PATCH sets outreachStatus, lastOutreachAt, outreachPhase; timeline Note documents send
Dry run on the same endpoint returns wouldSend true without SMTP—operators use it in PoC before the first live school. Validation failures set outreachStatus ERROR and outreachLastError so sales sees a visible blocker instead of a silent drop.
Sending domain and identities
Health endpoint GET /api/20tn/outreach/health reports list id, list tag, From, and Reply-To configured for the integration. Outbound product mail for 20.tn uses Stalwart on a dedicated sending subdomain—aligned with sales.dardev.net for human outreach channels—not Listmonk bulk from @dardev.net. Reply-To typically points at a monitored sales or send address so reply-sync jobs can attach responses to the Person record.
SPF, DKIM, and DMARC on that subdomain must pass before scaling send-one volume. DMARC quarantine and optional BIMI are documented separately; tracked TX does not fix authentication gaps.
Engagement sync back to CRM
POST /api/20tn/outreach/sync-engagement polls Listmonk view or campaign statistics for subscribers on the 20.tn list, maps by email, and PATCHes Person outreach engagement fields—OPENED, CLICKED, and downstream statuses used in workflow filters. Sales creates follow-up tasks when a director watches the demo video link but does not reply.
Listmonk webhook POST /webhooks/listmonk already writes CRM notes on unsubscribe; extending it for product:20tn:marketing sets consentEmail OPT_OUT and stops future TX. Unsubscribe is not a marketing-list annoyance—it is a consent state change crm-driven-product-lists treats as authoritative.
Operational boundaries
POST /api/20tn/outreach/send with limit and delayMs exists for batch orchestration behind mail jobs—it is not the default sales motion. Operators reach for send-one and workflow iterators so each school keeps a human approval trail. Bulk preview uses dry_run to count recipients without creating a Listmonk campaign that could be launched accidentally from the wrong UI screen.
DarDev Ops links health checks and bus APIs; it does not store SMTP passwords or edit From addresses—that stays in Stalwart, Listmonk, and env on dardev-vps. Template changes flow through git in listmonk/templates/ and deploy with the mail stack, not through ad hoc HTML in CRM.
How this differs from daily internal digest
Staff digest mail uses sync-worker POST /api/daily/send with DAILY_SEND_MODE direct SMTP from news@send.dardev.net—no Listmonk subscriber, no per-school CRM row. Outreach TX intentionally rides Listmonk so product engagement metrics stay comparable across Hesabi and 20.tn private lists when those integrations mature.
Is transactional mail the same as a marketing campaign?
No. Campaigns broadcast to many subscribers in one action. Transactional sends one message per API call with a template tied to a CRM personId—appropriate for approved outreach where each recipient has a distinct approval record.
When does a school appear on the Listmonk list?
After sync-20tn-outreach promotes them from CRM when consent and ICP filters pass. Import from ecoles.com.tn alone does not subscribe anyone—sync is a separate gated step.
Can we send tracked mail without Listmonk?
You lose unified open and click polling and webhook unsubscribe handling. DarDev standardizes on Listmonk TX for product outreach; direct SMTP remains for internal and warm-up paths documented in inside-unified-mailer-stack.
What placeholders must ops verify before go-live?
Video URL and length, sender name and title, WhatsApp and reply email env vars, and that __20TN_PERSONAL_LINE__ renders empty gracefully when CRM leaves the field blank.
Who should read this versus the sales playbook?
Engineers and ops configuring bus, templates, and sync read this guide. Sales staff running CRM workflows read 20tn-outreach-one-school-at-a-time; consent lawyers read crm-driven-product-lists.



