Dentolize · Send Template in Expired Conversation Walkthrough
On this pageBusiness viewTechnical view

Which Templates Qualify

Business view

Not every message the clinic has on file can be sent this way. Two separate gates decide what shows up in the Send Template picker:

  1. It has to be an approved WhatsApp template. Meta reviews and approves WhatsApp templates before a business can

use them outside the 24-hour window — that's a WhatsApp platform rule, not a Dentolize one. A message that hasn't been through that process (or was rejected, or is still pending) simply won't appear in the picker.

  1. It can only need the contact's name. Reminder messages are usually built from placeholders like the

appointment date, the doctor's name, or an invoice amount — placeholders that only make sense in the automated context (a cron job that has the actual appointment or invoice on hand). Outside that context, sending a message with @DATE in it would either send the literal text @DATE to the patient, or require staff to somehow supply the right value by hand. Rather than either of those, the picker simply won't let the message be sent — a message is only eligible if the only variables it contains are @PATIENT_NAME and/or @FIRST_NAME.

If a staff member picks a message that fails rule 2, they see exactly which placeholders can't be filled, right in the preview, and the Send button stays disabled. Rule 1 is enforced earlier, by the picker only ever showing approved messages in the first place.

Technical view

Rule 1 — approved template, enforced in the search query

The mobile and web pickers both call searchMessages with a new approved: true argument (packages/server/src/schema.graphql:24, added alongside the pre-existing searchTerm/onAction args). On the server, AppointmentQueries.searchMessages adds a where.AND clause when approved is set:

// packages/server/src/resolvers/queries/appointmentQueries.js:674-676
if (args.approved) {
  where.AND = [...where.AND, { template: { status: 'APPROVED' } }]
}

Message.template is a ConversationTemplate? relation; TemplateStatus is APPROVED | PENDING | REJECTED. A message with no template at all, or a template still pending or rejected, is filtered out here — before it ever reaches the client.

Rule 2 — name-only variables, enforced twice

Both apps compute this client-side for instant feedback, and the server re-derives and re-checks it — the client check is a UX nicety, not the source of truth.

  • Client. unfillableVariables(details) in each app's templateHelpers.js filters a fixed list of 25 known

@VARIABLE placeholders (the same list the reminder crons understand — company name, branch info, appointment date/time, doctor, patient/invoice IDs, points, etc.), keeping only the ones present in the message and not in NAME_VARIABLES = ['@PATIENT_NAME', '@FIRST_NAME'] (packages/clinic-mobile/src/components/dashboard/WhatsApp/conversationMessages/templateHelpers.js:1-45, and the byte-identical web copy). An empty result means the message is eligible.

  • Server. sendWhatsappTemplate re-extracts the variables actually present in the message using

extractValues, which was made exportable specifically for this reuse:

``js // packages/server/src/cronJobs/messages/embeddedValues.js:575 export const extractValues = text => { … } ``

and rejects the whole request if anything other than a name variable shows up:

```js // packages/server/src/resolvers/mutations/actions/officialWhatsApp/sendWhatsappTemplate.js:50-56 const variables = [...new Set(extractValues(messageData.details))]

if (variables.some(variable => !NAME_VARIABLES.includes(variable))) { throw new Error('Template Needs Variables[Client Error]') } ```

This is intentionally a duplicated, hand-maintained NAME_VARIABLES constant in three places (both templateHelpers.js files and the server resolver) rather than one shared list — see the note in the glossary if that surfaces as a maintenance question.

What "filling the name" actually means

Once a message passes both gates, the only substitution that happens is turning @PATIENT_NAME into the conversation's full name and @FIRST_NAME into the first word of it — computed identically in three places (mobile fillNameVariables, web fillNameVariables, and inline in the server resolver's valueFor helper at packages/server/src/resolvers/mutations/actions/officialWhatsApp/sendWhatsappTemplate.js:76). The name itself comes from the conversation's linked patient.fullName if there is one, falling back to the conversation's own name field (e.g. for a WhatsApp contact that never became a patient record) — see conversationTemplateName in templateHelpers.js and the equivalent inline lookup in the resolver (sendWhatsappTemplate.js:75).

What WhatsApp actually receives

The resolver doesn't send a free-form message with the name spliced in — it sends a type: "template" payload, naming the approved template (messageData.template.name) and its language, with the filled name as the template's one body parameter if a name variable was present (packages/server/src/resolvers/mutations/actions/officialWhatsApp/sendWhatsappTemplate.js:91-98). This is what lets WhatsApp accept it despite the conversation being expired — it's being sent as a template, which is exempt from the 24-hour rule, not as a disguised free-form message.