Skip to content

feat(automations): support contact details and conversation variables in webhook body template - #551

Open
shahidadev wants to merge 1 commit into
ArnasDon:mainfrom
shahidadev:main
Open

shahidadev wants to merge 1 commit into
ArnasDon:mainfrom
shahidadev:main

Conversation

@shahidadev

Copy link
Copy Markdown

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_webhook body 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 a send_webhook action, users configuring a custom body_template could 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

  1. Automation Engine (src/lib/automations/engine.ts):

    • Converted interpolate() into an async function with in-memory contact caching (resolveContact()) to avoid redundant database reads during a single execution run.
    • Added support for the following interpolation placeholders:
      • {{ 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> }}
    • Connected await interpolate() to send_webhook, send_message, update_contact_field, and create_deal.
    • Default fallback webhook payload now includes contact_id when body_template is omitted.
  2. Automation Builder UI (src/components/automations/automation-builder.tsx):

    • Added a ready-to-use JSON placeholder in the send_webhook step editor.
    • Added a helper hint listing all supported template variables.
  3. Unit Tests (src/lib/automations/engine.test.ts):

    • Added test coverage verifying that {{ 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 }}"
}

@shahidadev
shahidadev requested a review from ArnasDon as a code owner September 4, 2026 13:09
Copilot AI lite review requested due to automatic review settings September 4, 2026 13:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment on lines +798 to +814
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
}
Comment on lines +851 to +857
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] ?? '')
}
Comment on lines +1504 to +1509
<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 ArnasDon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. The UI hint renders its key path, not text. t("config.bodyTemplateHint", { defaultValue: ... }) — next-intl's t() has no defaultValue option; the second argument is ICU values. Automations.builder.config.bodyTemplateHint is not in messages/en.json or messages/ko.json, so users see the literal string Automations.builder.config.bodyTemplateHint and next-intl logs MISSING_MESSAGE. Add the key to both catalogues (the parity test in src/i18n/messages.test.ts requires both) and reference it plainly. The JSON example in the placeholder is fine to leave as a technical value.

  2. Contact cache leaks into serialised context (Copilot's first point is correct). resolveContact() writes _contact_cache onto args.context, and args.context is JSON-serialised in several places: the default send_webhook body (JSON.stringify({ ...args.context, contact_id })), the pending-execution row written for wait steps (pending.context), and automation logs. So a step-1 message using {{ contact.name }} followed by a step-2 webhook with no body_template posts the entire contacts row (select('*')) to the third-party URL. Keep the cache off context — e.g. a Map<string, ContactRow> on ExecuteArgs, or a module-level WeakMap keyed by args — and select only id, phone, name, email, company.

  3. 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.

  4. JSON-breaking values. body_template is 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants