Merge pull request #33 from ArnasDon/feat/first-inbound-message-trigger

feat(automations): add 'First Message from Contact' trigger
This commit is contained in:
Arnas Donauskas 2026-04-17 20:51:50 +03:00 committed by GitHub
commit a5f2a40b37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 36 additions and 5 deletions

View File

@ -383,6 +383,17 @@ async function processMessage(
? 'image' // stickers are images ? 'image' // stickers are images
: 'text' // reaction, unknown → text fallback : 'text' // reaction, unknown → text fallback
// Determine whether this is the contact's very first inbound message
// BEFORE we insert, so the count is accurate. Covers the case where
// the contact row already exists (manual add / CSV import) but they've
// never messaged us before — which new_contact_created wouldn't catch.
const { count: priorCustomerMsgCount } = await supabaseAdmin()
.from('messages')
.select('id', { count: 'exact', head: true })
.eq('conversation_id', conversation.id)
.eq('sender_type', 'customer')
const isFirstInboundMessage = (priorCustomerMsgCount ?? 0) === 0
const { error: msgError } = await supabaseAdmin().from('messages').insert({ const { error: msgError } = await supabaseAdmin().from('messages').insert({
conversation_id: conversation.id, conversation_id: conversation.id,
sender_type: 'customer', sender_type: 'customer',
@ -425,10 +436,20 @@ async function processMessage(
// Fire-and-forget: a slow or failing automation must not block the // Fire-and-forget: a slow or failing automation must not block the
// webhook's 200 OK response to Meta. // webhook's 200 OK response to Meta.
const inboundText = contentText ?? message.text?.body ?? '' const inboundText = contentText ?? message.text?.body ?? ''
const automationTriggers: ('new_contact_created' | 'new_message_received' | 'keyword_match')[] = const automationTriggers: (
contactOutcome.wasCreated | 'new_contact_created'
? ['new_contact_created', 'new_message_received', 'keyword_match'] | 'first_inbound_message'
: ['new_message_received', 'keyword_match'] | 'new_message_received'
| 'keyword_match'
)[] = ['new_message_received', 'keyword_match']
// new_contact_created fires only when the webhook just auto-created the
// contact row. first_inbound_message fires whenever this is the contact's
// first-ever customer-sent message — a superset that also catches
// manually-imported contacts sending for the first time. We dispatch both
// so users can pick whichever semantic they want; an automation that
// listens to only one trigger runs only when that trigger matches.
if (contactOutcome.wasCreated) automationTriggers.unshift('new_contact_created')
if (isFirstInboundMessage) automationTriggers.unshift('first_inbound_message')
for (const triggerType of automationTriggers) { for (const triggerType of automationTriggers) {
runAutomationsForTrigger({ runAutomationsForTrigger({
userId, userId,

View File

@ -106,8 +106,13 @@ const ADDABLE_STEPS: AutomationStepType[] = [
const TRIGGER_OPTIONS: { value: AutomationTriggerType; label: string; hint: string }[] = [ const TRIGGER_OPTIONS: { value: AutomationTriggerType; label: string; hint: string }[] = [
{ value: "new_message_received", label: "New Message Received", hint: "Any incoming message" }, { value: "new_message_received", label: "New Message Received", hint: "Any incoming message" },
{
value: "first_inbound_message",
label: "First Message from Contact",
hint: "First time this contact ever messages you (works for manually-added contacts too)",
},
{ value: "keyword_match", label: "Keyword Match", hint: "Message contains specific keyword(s)" }, { value: "keyword_match", label: "Keyword Match", hint: "Message contains specific keyword(s)" },
{ value: "new_contact_created", label: "New Contact Created", hint: "When a contact is auto-created" }, { value: "new_contact_created", label: "New Contact Created", hint: "When a contact is auto-created from an incoming message" },
{ value: "conversation_assigned", label: "Conversation Assigned", hint: "When assigned to an agent" }, { value: "conversation_assigned", label: "Conversation Assigned", hint: "When assigned to an agent" },
{ value: "tag_added", label: "Tag Added", hint: "When a tag is added to a contact" }, { value: "tag_added", label: "Tag Added", hint: "When a tag is added to a contact" },
{ value: "time_based", label: "Time-Based", hint: "On a recurring schedule" }, { value: "time_based", label: "Time-Based", hint: "On a recurring schedule" },

View File

@ -11,6 +11,10 @@ export const TRIGGER_META: Record<AutomationTriggerType, TriggerMeta> = {
label: 'New Message', label: 'New Message',
pillClass: 'border-blue-500/30 bg-blue-500/10 text-blue-300', pillClass: 'border-blue-500/30 bg-blue-500/10 text-blue-300',
}, },
first_inbound_message: {
label: 'First Message from Contact',
pillClass: 'border-teal-500/30 bg-teal-500/10 text-teal-300',
},
keyword_match: { keyword_match: {
label: 'Keyword Match', label: 'Keyword Match',
pillClass: 'border-purple-500/30 bg-purple-500/10 text-purple-300', pillClass: 'border-purple-500/30 bg-purple-500/10 text-purple-300',

View File

@ -214,6 +214,7 @@ export interface BroadcastRecipient {
export type AutomationTriggerType = export type AutomationTriggerType =
| 'new_message_received' | 'new_message_received'
| 'first_inbound_message'
| 'keyword_match' | 'keyword_match'
| 'new_contact_created' | 'new_contact_created'
| 'conversation_assigned' | 'conversation_assigned'