All playbooks
Playbook 2 of 6
Route transactional messages through triggers
Move message content out of your codebase. Each transactional message becomes a trigger key and a handful of variables. The copy itself moves to Messy, where you can edit it without a deploy.
≈ 20 min You ship: Every transactional call site reduced to one line: trigger the event, pass the variables.
1
Inventory what you send
First, a read-only audit. Your agent maps every message the app sends today and proposes a trigger key and variable list for each. The output file drives the next two playbooks, so keep it in the repo.
01 · audit transactional messages
Audit this codebase and inventory every transactional message it sends to users: emails (mailers, SMTP calls, SendGrid/SES/Postmark SDK calls), SMS, WhatsApp and push notifications.
Produce a markdown table with one row per distinct message:
- event: what happens in the product (user requests a login code, order ships, ...)
- channel: email, sms, whatsapp or push
- current implementation: the file and method that builds and sends it today
- trigger: a proposed trigger key, dotted and lifecycle-based, like login.otp, login.magic_link, account.email_verify, billing.invoice_paid, order.shipped. Keys must be unique per channel.
- variables: the dynamic values the template will need, as a flat list of snake_case names (name, otp_code, invoice_url, ...). Prefer ready-made URLs and formatted strings over raw objects.
Do not change any code yet. Save the table to docs/messy-messages.md; the next steps build templates and call sites from it.2
Swap the call sites
Now the migration itself. App code stops knowing what messages look like; it only knows that an event happened and which variables describe it.
02 · migrate call sites
Using the inventory in docs/messy-messages.md and the MessyClient in this codebase, migrate every transactional message to Messy triggers.
For each row:
- Replace the current mailer or SDK call with a call to the Messy client's trigger method: the trigger key from the table, the channel, the recipient, and a flat data object with exactly the variables listed.
- Route calls through the background job wrapper, except OTP and other login-critical sends, which stay synchronous.
- Delete the now-dead mailer views/templates and provider SDK code paths once nothing references them. Leave provider config in place until the Messy templates are live.
Constraints:
- Do not invent new trigger keys; keep them exactly as inventoried, and update the inventory if a key has to change.
- Build URLs app-side and pass them as variables (magic_link_url, invoice_url). Templates never construct URLs.
- Keep the diff reviewable: one commit per message family (auth, billing, orders) if the repo uses small commits.3
WhatsApp is different
Meta requires pre-approved business templates for WhatsApp, so those call sites do not use triggers. They use the raw send, with the approved template name in subjectand Meta's component parameters in tags.
03 · whatsapp templates
Update the WhatsApp call sites from the inventory to use Messy's raw send with Meta-approved business templates.
POST <MESSY_API_URL>/messages with body:
{"type": "whatsapp", "message": {"to": "31612345678", "subject": "login_otp", "language": "en", "tags": [{"type": "body", "parameters": [{"type": "text", "text": "123456"}]}]}}
- "subject" carries the approved template's name, "language" its locale code, and "tags" the Meta component array: body parameters first, then url-button parameters if the template has them, in template order.
- Normalize phone numbers to digits only, no plus sign, before sending.
- Add one client convenience method send_whatsapp_template(to, template, language, components) so call sites stay tidy.
- You can list the approved templates and their placeholders with GET <MESSY_API_URL>/whatsapp_templates.