feat(automations): add 'First Message from Contact' trigger

new_contact_created only fires when the webhook auto-creates the
contact row, so it misses the case of manually-added / imported
contacts sending their first-ever message.

first_inbound_message fires whenever the incoming message is the
contact's first customer-sent message (checked by counting prior
customer-sender messages in the conversation before insert), which
covers both auto-created and pre-existing contacts.

- Added first_inbound_message to AutomationTriggerType.
- New pill color + dropdown entry labeled "First Message from Contact".
- Webhook now counts prior customer messages before inserting the
  incoming one; dispatches first_inbound_message when the count is 0.
- new_contact_created still fires for brand-new contacts; they're
  independent triggers with overlapping coverage, so users pick.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arnas Donauskas 2026-04-17 20:47:13 +03:00
parent da809ced5f
commit 5243188d0c
No known key found for this signature in database
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'