Skip to content

Channel Routing

When an inbound SMS arrives, Loquent uses a two-tier lookup to find which text agent should handle it. Understanding this hierarchy is key to configuring agents correctly.

Inbound SMS on phone number P from contact C
├─ 1. Contact assignment: contact_text_agent
│ WHERE contact_id = C AND channel = "sms"
│ → Found: use this agent + its auto_reply setting
└─ 2. Phone-level default: phone_number.text_agent_id
WHERE phone_number.id = P
→ Found: use this agent + phone_number.text_agent_auto_reply
→ Not found: skip — no suggestions generated

A contact assignment always wins over the phone default. The phone-level agent only applies when no explicit assignment exists for that contact + channel pair.

Each phone number can have a text agent assigned as its org-wide default for inbound SMS. This is set in the Phone Details view under Text Agent.

  • Assign: PUT /api/phones/{id} with text_agent_id set
  • Auto-reply: the text_agent_auto_reply toggle on the phone record controls whether the agent sends replies automatically for all contacts without an explicit assignment
  • Clear: PUT /api/phones/{id} with text_agent_id: null

See Phone for full details on the phone-level assignment UI and API.

Explicit per-contact assignments override the default and unlock auto-reply.

Via the UI: Open a contact’s detail page → sidebar → Text Agents section. Each channel (SMS, WhatsApp) has a dropdown:

  • Select an agent → creates/replaces the assignment for that channel
  • Select None → removes the assignment, falling back to the org default
  • When no explicit assignment exists, the dropdown shows the org default name in italics

Via API:

POST /api/contacts/{contact_id}/text-agents
Body: {
text_agent_id: "...",
channel: "sms",
auto_reply: false
}

This is an upsert — submitting a new assignment for an existing (contact, channel) pair replaces it atomically (delete + insert in a transaction).

DELETE /api/contacts/{contact_id}/text-agents/{channel}
GET /api/contacts/{contact_id}/text-agents

Returns all assignments for the contact across all channels.

-- Contact assignment (one per contact + channel)
CREATE TABLE contact_text_agent (
id UUID PRIMARY KEY,
organization_id UUID NOT NULL,
contact_id UUID NOT NULL REFERENCES contact(id) ON DELETE CASCADE,
text_agent_id UUID NOT NULL REFERENCES text_agent(id) ON DELETE CASCADE,
channel TEXT NOT NULL, -- "sms" | "whatsapp"
auto_reply BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP NOT NULL,
UNIQUE (contact_id, channel)
);
-- Org default per channel (one per org + channel)
CREATE TABLE text_agent_default_channel (
id UUID PRIMARY KEY,
organization_id UUID NOT NULL REFERENCES organization(id) ON DELETE CASCADE,
channel TEXT NOT NULL, -- "sms" | "whatsapp"
text_agent_id UUID NOT NULL REFERENCES text_agent(id) ON DELETE CASCADE,
created_at TIMESTAMP NOT NULL,
UNIQUE (organization_id, channel)
);

Auto-reply sends the highest-confidence suggestion automatically when a message arrives.

Enabling auto-reply:

Auto-reply can be enabled at two levels:

  • Contact assignment — toggle in the contact sidebar, or auto_reply: true in the upsert API. Applies only to that specific contact.
  • Phone numbertext_agent_auto_reply on the phone record. Applies to all contacts handled by the phone-level default (i.e. those without an explicit contact assignment).

How it behaves:

ConditionResult
auto_reply = falseSuggestions generated and saved; human selects from the messaging view
auto_reply = true, confidence ≥ thresholdBest suggestion sent automatically; text_agent_suggestion.auto_sent_body set
auto_reply = true, confidence < thresholdBest suggestion still sent; a warning alert note is added to the contact record
No agent configuredNothing — message sits in the inbox

The threshold is configured per agent (text_agent.confidence_threshold, default 0.7). See Configuration for how to tune it.

ActionPermission Required
Set default channelTextAgent:Collection:Create
Remove default channelTextAgent:Instance:Update
Assign agent to contactTextAgent:Instance:Update
Remove contact assignmentTextAgent:Instance:Update
List contact assignments(no ABAC check — session org-scoped)
src/mods/text_agent/
├── api/
│ ├── get_contact_text_agents_api.rs # GET /api/contacts/{id}/text-agents
│ ├── upsert_contact_text_agent_api.rs # POST /api/contacts/{id}/text-agents
│ ├── delete_contact_text_agent_api.rs # DELETE /api/contacts/{id}/text-agents/{ch}
│ ├── get_text_agent_default_channels_api.rs # GET /api/text-agents/defaults
│ ├── set_text_agent_default_channel_api.rs # POST /api/text-agents/defaults
│ └── delete_text_agent_default_channel_api.rs # DELETE /api/text-agents/defaults/{ch}
├── types/
│ ├── contact_text_agent_type.rs
│ ├── contact_text_agent_data_type.rs
│ ├── text_agent_default_channel_type.rs
│ └── text_agent_default_channel_data_type.rs
└── components/
└── contact_text_agent_section_component.rs # Contact sidebar UI