feat(automations): support contact details and conversation variables in webhook body template - #551
shahidadev wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The current contact caching and contact.* interpolation behavior can unintentionally expose extra contact data (including via the default webhook payload) and should be tightened before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds richer template interpolation for automation actions (notably send_webhook) so webhook payloads, messages, and deal titles can include contact attributes and conversation context from the triggering event.
Changes:
- Made
interpolate()async and added contact resolution with per-execution caching. - Expanded supported placeholders to include
contact.*,conversation.id, and shorthand variants; wired interpolation into multiple step types. - Updated the automation builder webhook editor UX (placeholder JSON + supported-variable hint) and added unit coverage for webhook interpolation.
File summaries
| File | Description |
|---|---|
src/lib/automations/engine.ts |
Async interpolation + contact/conversation placeholder support; webhook default payload now includes contact_id. |
src/lib/automations/engine.test.ts |
Adds a unit test asserting webhook body_template interpolation for contact + message + conversation. |
src/components/automations/automation-builder.tsx |
Improves webhook step editor with URL placeholder, body template sample, and supported-variable hint. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| async function resolveContact(args: ExecuteArgs): Promise<Record<string, unknown> | null> { | ||
| if (!args.contactId) return null | ||
| const cache = args.context as { _contact_cache?: Record<string, unknown> } | ||
| if (cache._contact_cache) return cache._contact_cache | ||
|
|
||
| const db = supabaseAdmin() | ||
| const { data, error } = await db | ||
| .from('contacts') | ||
| .select('*') | ||
| .eq('id', args.contactId) | ||
| .eq('account_id', args.automation.account_id) | ||
| .maybeSingle() | ||
|
|
||
| if (error || !data) return null | ||
| cache._contact_cache = data as Record<string, unknown> | ||
| return cache._contact_cache | ||
| } |
| if (prop === 'phone' || prop === 'number') return String(contactRecord.phone ?? '') | ||
| if (prop === 'name') return String(contactRecord.name ?? '') | ||
| if (prop === 'email') return String(contactRecord.email ?? '') | ||
| if (prop === 'company') return String(contactRecord.company ?? '') | ||
| if (prop === 'id') return String(contactRecord.id ?? args.contactId ?? '') | ||
| return String(contactRecord[rawProp] ?? contactRecord[prop] ?? '') | ||
| } |
| <p className="mt-1 text-[11px] text-muted-foreground"> | ||
| {t("config.bodyTemplateHint", { | ||
| defaultValue: | ||
| "Supported: {{ contact.phone }}, {{ contact.name }}, {{ contact.email }}, {{ message.text }}, {{ conversation.id }}, {{ vars.* }}", | ||
| })} | ||
| </p> |
ArnasDon
left a comment
There was a problem hiding this comment.
Thanks — forwarding the sender's phone/name to a webhook is a reasonable gap to close, and the engine test is welcome. I merged this onto current main: clean merge, eslint 0 errors, tsc clean, vitest 993/993. A few things need to change before it can land, in priority order:
-
The UI hint renders its key path, not text.
t("config.bodyTemplateHint", { defaultValue: ... })— next-intl'st()has nodefaultValueoption; the second argument is ICU values.Automations.builder.config.bodyTemplateHintis not inmessages/en.jsonormessages/ko.json, so users see the literal stringAutomations.builder.config.bodyTemplateHintand next-intl logsMISSING_MESSAGE. Add the key to both catalogues (the parity test insrc/i18n/messages.test.tsrequires both) and reference it plainly. The JSON example in theplaceholderis fine to leave as a technical value. -
Contact cache leaks into serialised context (Copilot's first point is correct).
resolveContact()writes_contact_cacheontoargs.context, andargs.contextis JSON-serialised in several places: the defaultsend_webhookbody (JSON.stringify({ ...args.context, contact_id })), the pending-execution row written forwaitsteps (pending.context), and automation logs. So a step-1 message using{{ contact.name }}followed by a step-2 webhook with nobody_templateposts the entirecontactsrow (select('*')) to the third-party URL. Keep the cache offcontext— e.g. aMap<string, ContactRow>onExecuteArgs, or a module-levelWeakMapkeyed byargs— and select onlyid, phone, name, email, company. -
Whitelist the supported properties. The
contactRecord[rawProp] ?? contactRecord[prop]fallback lets a template pull any column (wa_user_id,metadata, notes, custom fields…) into an outbound payload. Return''for anything not in the documented set. -
JSON-breaking values.
body_templateis substituted as raw text, so a contact name containing"or a newline produces invalid JSON at the destination. This was already true for{{ message.text }}, but names make it far more likely. Escaping interpolated values when the template is JSON (e.g.JSON.stringify(value).slice(1, -1)) would fix it.
Smaller: the hint omits {{ contact.company }} / {{ contact.id }} and the undocumented {{ contact.number }} alias; and lower-casing vars keys makes {{ vars.Foo }} / {{ vars.foo }} collide — keep vars lookups case-sensitive as before.
With 1–3 addressed (4 ideally) I'm glad to take this.
Summary
This PR enhances the automation engine and builder UI to support dynamic contact attributes (such as sender phone number, name, email, company, and contact ID) and conversation context within automation
send_webhookbody templates (as well as messages and deal titles).Motivation
Previously, the automation engine's
interpolate()helper only supported{{ message.text }}and{{ vars.* }}. When creating automations with asend_webhookaction, users configuring a custombody_templatecould not forward the sender's phone number ({{ contact.phone }}) or contact details to external webhook destinations (such as Zapier, Make, n8n, or custom endpoints).Changes
Automation Engine (
src/lib/automations/engine.ts):interpolate()into anasyncfunction with in-memory contact caching (resolveContact()) to avoid redundant database reads during a single execution run.{{ contact.phone }}/{{ phone }}{{ contact.name }}/{{ name }}{{ contact.email }}/{{ email }}{{ contact.company }}/{{ company }}{{ contact.id }}/{{ contact_id }}{{ conversation.id }}/{{ conversation_id }}{{ message.text }}/{{ message }}{{ vars.<key> }}await interpolate()tosend_webhook,send_message,update_contact_field, andcreate_deal.contact_idwhenbody_templateis omitted.Automation Builder UI (
src/components/automations/automation-builder.tsx):send_webhookstep editor.Unit Tests (
src/lib/automations/engine.test.ts):{{ contact.phone }},{{ contact.name }},{{ message.text }}, and{{ conversation.id }}correctly interpolate into webhook payloads.Example Configuration
Users can now specify custom body templates in the automation builder:
{ "sender_phone": "{{ contact.phone }}", "sender_name": "{{ contact.name }}", "message": "{{ message.text }}", "conversation_id": "{{ conversation.id }}" }