Skip to main content

The keyword-rules matching model

A keyword rule is a per-tenant dispatch rule that reads the body of every inbound (MO) message and decides what one match should do: send an auto-reply, record an opt-in or opt-out, forward the turn to a deployed AI agent, or start an automation flow. The active rule set is consulted once per inbound message, and at most one rule fires. This page defines the model itself — the match semantics, the evaluation order, the consent writes, and what a matched action dispatches. The two guides — Auto-reply rules (form setup) and Keyword rules worked examples (API walkthroughs) — are exercises over this model; this page is the semantics they assume.

What a keyword rule is

Each rule is one row in your organization’s rule table, created through the dashboard (Settings → Auto-Reply Rules) or POST /api/v1/settings/keyword-rules. The fields: Rules are scoped to your whole organization, not to a specific number: a rule fires on every reply-capable number you have on its channel. The select clause for evaluation is deliberately narrow — active = true and the message’s channel — so only enabled rules on the right channel are ever compared. Two small normalization rules shape what the stored action value means:
  • The legacy aliases forward and agent both mean forward_agent; flow means trigger_flow. Both the read and the write path normalize, so rows saved with the aliases and rows saved with the canonical values behave identically.
  • An opt-out rule writes suppression (see below); a reply rule sends a billed message. The executor compares against the canonical vocabulary above.

Match semantics

The matcher folds case on both sides of the comparison, so hours, HOURS, and Hours are the same keyword on every match type. Leading and trailing whitespace on the message body is trimmed before an exact or starts_with comparison. Two properties of the matcher carry over from the implementation and matter for what you can rely on:
  • Case folding is Unicode-aware in both branches — the same toUpperCase fold runs on the rule keyword and the message body for exact, contains, and starts_with. Accents stay distinct (CAFECAFÉ); if your audience uses both, add one rule per variant or use a regex like caf[eé].
  • A disabled or broken rule never blocks evaluation. A stored regex pattern that fails to compile — or one the platform’s ReDoS safety gate flags (nested quantifiers such as (a+)+, alternation inside a quantifier such as (ab|a)*) — is skipped and logged once, and the remaining rules still evaluate normally. The save path rejects those patterns outright, and the match path re-checks them, so a dangerous pattern can never stall message processing.

Evaluation order and precedence

Evaluation is first-match-wins in creation order, oldest first. The active rules for the message’s channel are loaded in creation order, each rule is checked in sequence, and the first rule whose match type succeeds terminates evaluation — one inbound message fires at most one rule. Consequences to design around:
  • Specific beats general only when the specific rule is older. An exact: YES created before a contains: YES wins the bare YES; reverse the creation order and the contains rule swallows every YES-form. To re-sequence, delete the broad rule and re-create it so it becomes the newest.
  • Ties break by creation order, too. Two rules with the same keyword and match type both exist? The older one wins; the newer one never fires while the older stays active. The list endpoint returns newest first for management, so read a page from the bottom up to reconstruct the evaluation order.
  • The catch-all belongs last. A broad contains rule or a .* regex created after your specific keywords only claims what nothing earlier matched.
Rules live in memory for a short while (a brief per-channel cache), and any create, update, or delete evicts it immediately — an edit takes effect on the very next inbound message. Before any of your rules run, the platform’s built-in consent vocabulary (multilingual STOP/START and equivalents) is recognized and processed as an opt-out or opt-in directly — the rules layer is never consulted for those words, and a rule on STOP will never fire. Your own consent vocabulary is where keyword rules come in: a rule with action: "opt-out" or action: "opt-in" teaches the platform a brand-specific word the built-in layer doesn’t know (UNSUBSCRIBE, JOIN, JOIN THE CLUB). When an opt-out rule matches, the same canonical consent write the built-in STOP handling uses runs:
  1. The contact’s channel preferences flip — the per-channel flag is set to opted out, and the organization’s global opt-out flag is set, so the write blocks sends to that contact on every channel, not only the one the message arrived on. A consent-history entry records the keyword and timestamp.
  2. An explicit consent record is appended — the audit-grade evidence for the revoke, storable in the consent_records ledger that Consent, opt-out, and suppression describes.
  3. A suppression entry is created — the send gate checks this before any dispatch, so every later send to that address is refused pre-dispatch with RECIPIENT_OPTED_OUT instead of being delivered and billed.
Then, if the rule has response_text, the confirmation goes out as the one outbound send an opt-out keyword is allowed to make. Opt-out and opt-in acknowledgements intentionally bypass the billed send path — confirmation of a consent change is free by platform design. An opt-in rule matches the reverse: the contact’s flags clear, a fresh consent record is written, and the matching suppression entries are revoked (marked revoked, never deleted) so the ledger keeps the full history. Pair every custom opt-out keyword with its opt-in counterpart so a contact always has a way back — nothing stops them from texting the words you tell them about, and only an explicit re-consent clears the block. For how these stores interact with quiet hours, frequency caps, and duplicate suppression, see Consent, opt-out, and suppression; the rules layer only decides which consent write happens.

How a matched rule dispatches

The action decides the dispatch target:
  • reply — the response text, rendered, is sent through the same billed outbound pipeline as any other message: wallet, quota, and usage gates all run. It appears in the Inbox thread and the Delivery log as an ordinary outbound message.
  • forward_agent — the turn is handed to a deployed AI agent by agent_id, so an inbound keyword acts as a routing trigger into the agent-runtime (useful when the keyword signals a human-handoff intent, e.g. SUPPORT).
  • trigger_flow — an automation flow by flow_id starts with the inbound message as its context; the run’s context records the matched keyword, and the flow’s history shows every keyword-triggered run.
  • opt-out / opt-in — the consent pipeline above, plus an optional confirmation reply.
A reply body’s text can interpolate the matched keyword, the sender’s number, the channel, and the two endpoints with {{...}} tokens — see the worked examples for the token list. An unrecognized token resolves to an empty string, never a literal {{...}} left in the outbound. A message no rule matches falls through to the channel’s other inbound handling — a deployed AI agent if the channel has one, otherwise the Inbox for a human. Keyword rules are first in that chain, so an accurate rule set decides earlier than the agent.

Walkthrough: an inbound opt-out, evaluated pre-dispatch

Set the stage with one rule per match type (all on sms), created in this order:
Exact — match the whole trimmed body:
Contains — the keyword anywhere in the body:
Starts-with — the keyword at the front of the trimmed body:
Now an inbound please unsubscribe me arrives on your SMS number. The evaluation runs pre-dispatch, before the message reaches a human’s queue:
  1. Built-in consent layer first. please unsubscribe me contains no built-in word — the built-in layer releases it to rules.
  2. Rules evaluate oldest-first. The HOURS exact rule compares the trimmed body and fails. The UNSUBSCRIBE contains rule finds its keyword mid-body and matches; evaluation stops — the SUPPORT starts_with rule is never checked.
  3. The matched action dispatches. opt-out runs the consent pipeline: the contact’s channel preferences flip to opted out globally, a consent record is appended, a suppression entry is created, and the confirmation — rendered from the rule’s text — goes back over the same channel because the rule had response_text.
From then on, every send request naming that contact is refused with RECIPIENT_OPTED_OUT until a matching opt-in rule (or the Consent API) re-consents. Confirm in the Opt-Outs view — the record lands with method keyword and the matched word.

Tenant-owned control

Keyword rules are your organization’s own dispatch policy: which words trigger what, when the replies send, and what the replies say are all tenant-owned choices. The built-in STOP/START vocabulary aside, no rule exists until you create one. Recording consent changes through an opt-out keyword builds the audit trail, but as with any consent record it does not itself make a send lawful — you remain responsible for the initial consent capture and for message content.

See also