Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/rotten-melons-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@getmunin/backend-core': minor
---

SMS outreach campaigns

An outreach campaign can now run on an SMS channel. `outreach_propose_initial` drafts a text against a contact's phone number, and a person approving it in the dashboard creates the outbound conversation and sends the message. As with voice, an agent cannot approve one.

Drafts are capped at 480 characters — roughly three billable segments — and rejected above it, with the limit stated in the tool schema so a curator can write to it rather than discover it. Composition is SMS-shaped: the campaign CTA is appended as a bare URL and the opt-out line as `Reply STOP to opt out.`, instead of the markdown link footer an email gets. `skill://outreach/draft-initial-sms` covers the writing rules, including why an emoji triples the cost of a message.

**Outbound SMS was never actually delivered.** `ConvService` enqueued a `conv_message_deliveries` row only for `channelType === 'email'`, so an agent or operator replying on an SMS conversation stored a message that silently never sent — the `OutboundDeliveryWorker` and both SMS adapters were fine, nothing was ever handed to them. The gate now covers email and SMS.

Follow-up sequences stay email-only: an SMS campaign rejects `sequenceSteps`, and `outreach_propose_followup` still refuses a text conversation. One touch, then the reply flow, which works because inbound texts thread into the outreach conversation.

`findOrCreateContactByPhone` was duplicated in the Vapi and Threll adapters and inlined a third time in the outreach voice path; it is now one shared helper taking the source as a parameter.
6 changes: 3 additions & 3 deletions packages/backend-core/docs-fixtures/mcp-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -6797,7 +6797,7 @@
{
"name": "outreach_create_campaign",
"title": "Outreach: Create campaign",
"description": "Create an outbound-campaign definition. Operators write `brief` as a one-paragraph human description of intent (the curator personalises per contact from this). `segmentId` chooses the audience; the curator calls `crm_list_contacts_in_segment` (which always enforces suppression+consent floor) to materialize it. `channelId` must reference an email channel. New campaigns default `enabled: false` so nothing sends until you flip it on. Automation is opt-in per behavior: `autoDraftInitial` defaults false (the weekly curator does not draft first-touch emails until you set it true — draft manually otherwise), while `autoDraftReplies` defaults true (replies to inbound prospect messages are auto-drafted for review). Optional `sequenceSteps` (email campaigns only) defines a follow-up sequence — each step is a wait period plus a drafting brief; defining steps on an enabled campaign opts it into daily follow-up drafting for threads with no reply.",
"description": "Create an outbound-campaign definition. Operators write `brief` as a one-paragraph human description of intent (the curator personalises per contact from this). `segmentId` chooses the audience; the curator calls `crm_list_contacts_in_segment` (which always enforces suppression+consent floor) to materialize it. `channelId` must reference an email, SMS, or voice channel; approving a proposal on an SMS or voice campaign is restricted to a signed-in person in the Munin dashboard. New campaigns default `enabled: false` so nothing sends until you flip it on. Automation is opt-in per behavior: `autoDraftInitial` defaults false (the weekly curator does not draft first-touch emails until you set it true — draft manually otherwise), while `autoDraftReplies` defaults true (replies to inbound prospect messages are auto-drafted for review). Auto-sending a reply is not an option on any campaign: conversations created by an approved proposal are always set to `draft_only`, whatever the channel default says, so a prospect never receives an unreviewed reply. Optional `sequenceSteps` (email campaigns only) defines a follow-up sequence — each step is a wait period plus a drafting brief; defining steps on an enabled campaign opts it into daily follow-up drafting for threads with no reply.",
"audiences": [
"admin"
],
Expand Down Expand Up @@ -7581,15 +7581,15 @@
"maxLength": 64
},
"draftSubject": {
"description": "Required for email campaigns; omit for voice campaigns where the call has no subject.",
"description": "Required for email campaigns; omit for SMS and voice campaigns, which have no subject.",
"type": "string",
"maxLength": 300
},
"draftBody": {
"type": "string",
"minLength": 1,
"maxLength": 20000,
"description": "For email campaigns: the email body. For voice campaigns: the opening line / talking-points the AI agent should use when the call connects."
"description": "For email campaigns: the email body. For SMS campaigns: the text message, capped at 480 characters, plain text — no markdown, and no opt-out line, which Munin appends. For voice campaigns: the opening line / talking-points the AI agent should use when the call connects."
},
"evidence": {
"type": "object",
Expand Down
Loading
Loading