SMS & Campaigns Schema Gotchas

This page documents the non-obvious quirks in the SMS and campaigns tables. An AI agent or developer who skips this page will write broken queries.

campaigns_items uses customer_id, not id_customer

The foreign key linking a campaign item to a contact is named customer_id — not id_customer, not contact_id.

-- Wrong — column does not exist
SELECT * FROM campaigns_items WHERE id_customer = :contact_id;

-- Wrong — also does not exist
SELECT * FROM campaigns_items WHERE contact_id = :contact_id;

-- Correct
SELECT * FROM campaigns_items WHERE customer_id = :contact_id;

This is the single most common query error in the campaigns system.

smslog.text is a BLOB

The text column in smslog stores message content as a BLOB, not VARCHAR or TEXT. This has two consequences:

1. String comparisons require CONVERT

-- Wrong — comparison silently fails or returns unexpected results
SELECT * FROM smslog WHERE text LIKE '%follow up%';

-- Correct
SELECT * FROM smslog
WHERE CONVERT(text, CHAR) LIKE '%follow up%';

2. Output is binary unless cast

When selecting message content to display or process, always cast:

SELECT
  id,
  CONVERT(text, CHAR) AS message,
  direction,
  created_at
FROM smslog
WHERE id_company = :id_company
  AND deleted_at IS NULL
ORDER BY created_at DESC;

Forgetting the cast returns a binary buffer object in application code instead of a string. This will not throw an error — it will silently display as garbled output or [object Object].

campaigns_items soft deletes

campaigns_items follows the standard soft-delete pattern. Always include deleted_at IS NULL:

SELECT * FROM campaigns_items
WHERE id_company = :id_company
  AND customer_id = :contact_id
  AND deleted_at IS NULL;

Campaign status values

The status column on campaigns uses these exact strings:

Value Meaning
draft Not yet scheduled
scheduled Queued for future send
sending Currently in progress
sent Completed
paused Manually paused mid-send
failed Send failed — check error_message

Filtering for active campaigns:

SELECT * FROM campaigns
WHERE id_company = :id_company
  AND status IN ('scheduled', 'sending')
  AND deleted_at IS NULL;

Template variable substitution

sms_templates.body uses double-curly syntax:

Hi {{first_name}}, your appointment is on {{date}}.

Substitution happens at send time. Available variables:

Variable Source
{{first_name}} contacts.first_name
{{last_name}} contacts.last_name
{{phone}} contacts.phone
{{email}} contacts.email
{{company}} companies.name linked to the contact

Unresolved variables (contact missing the field) render as an empty string — not as the raw {{variable}} placeholder.

Full contact SMS thread query

The correct pattern to fetch a complete SMS thread for a contact:

SELECT
  id,
  CONVERT(text, CHAR) AS message,
  direction,
  created_at
FROM smslog
WHERE id_company = :id_company
  AND contact_id = :contact_id
  AND deleted_at IS NULL
ORDER BY created_at ASC;