Triggers¶
Triggers & events covers the
conceptual model in prose, condition operators, condition_logic,
action_type, context_template rendering, cooldown_seconds, with no
endpoint examples. Read that page first if you haven't. This page assumes
it and is the full CRUD reference: creating, listing, updating, deleting,
and dry-run testing a Trigger against real endpoints.
Endpoint family¶
POST /api/v1/agents/{agent_id}/triggers
GET /api/v1/agents/{agent_id}/triggers
GET /api/v1/agents/{agent_id}/triggers/{trigger_id}
PATCH /api/v1/agents/{agent_id}/triggers/{trigger_id}
DELETE /api/v1/agents/{agent_id}/triggers/{trigger_id}
POST /api/v1/agents/{agent_id}/triggers/{trigger_id}/test
Authentication and roles¶
Every endpoint here takes an admin session token (see Authentication), not an API key. Role requirements split by whether the call changes anything:
| Endpoint | Required role |
|---|---|
GET (list, detail) |
Owner, Admin, Editor, or Viewer |
POST (create) |
Owner, Admin, or Editor |
PATCH (update) |
Owner, Admin, or Editor |
DELETE |
Owner, Admin, or Editor |
POST .../test |
Owner, Admin, or Editor |
A Viewer can read Trigger configuration but can't create, edit, delete, or
even dry-run one. The /test endpoint is held to the same role
requirement as the mutating endpoints rather than treated as read-only,
because it still renders context_template and resolves a real phone
number from whatever payload you give it.
Every create, update, and delete also writes an audit log
entry (resource_type: "trigger").
Create a Trigger¶
Capped by your licence
This deployment's licence sets a triggers limit. Creating one more
past that cap returns 402 rather than succeeding; see
Licence limits.
Field reference¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | Yes | A label, shown in the admin dashboard and in audit log entries. | |
enabled |
boolean | No | true |
A disabled Trigger is never evaluated against incoming events. |
event_type_filter |
string or null | No | null (matches every event_type) |
Only evaluate this Trigger for events posted with this exact event_type. |
conditions |
list of condition objects | No | [] (always matches) |
Checked against the posted payload under condition_logic. See the exact shape of each object directly below. |
condition_logic |
"all" or "any" |
No | "all" |
Whether every condition must match, or just one. |
phone_field_path |
string | Only meaningful for call, whatsapp, and both |
"phone" |
Dot-path into the posted payload where the recipient's phone number lives. A match with no valid number here doesn't hard-fail, it's dropped and recorded on the event. |
from_number |
string or null | No | null (falls back to the agent's own twilio_phone_number) |
Override which of your numbers places the call or sends the WhatsApp message. |
context_template |
string or null | Required for agent_task; optional otherwise |
null |
Jinja2, rendered against the event payload. For call/both it becomes the agent's dynamic opening context; for agent_task it's the literal task instruction the agent executes headlessly. |
cooldown_seconds |
integer ≥ 0 or null | No | null (no cooldown) |
Suppresses a repeat fire to the same resolved phone number within this window. Meaningless for agent_task (no recipient to dedupe on). |
action_type |
"call", "whatsapp", "both", "telegram", or "agent_task" |
No | "call" |
Determines which fields above are actually required; see below. |
whatsapp_template_sid |
string or null | Required for whatsapp/both |
null |
A pre-approved Twilio Content Template SID. WhatsApp/Meta policy requires one for any business-initiated message. |
whatsapp_template_variables_template |
string or null | No | null |
Jinja2, must render to a JSON object ({"1": "...", "2": "..."}) filling the Content Template's numbered variables. |
telegram_chat_id |
string or null | No | null (falls back to the deployment's TELEGRAM_CHAT_ID) |
The chat to message. |
telegram_chat_id_field_path |
string or null | No | null |
Dot-path into the posted payload for a chat id. When it resolves, it wins over telegram_chat_id, so one Trigger can message whichever person the event is about. |
telegram_message_template |
string or null | Required for telegram |
null |
Jinja2, rendered against the payload into the message body. |
The shape of a condition object¶
Each entry in conditions is an object with three fields.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
field |
string | Yes (minimum 1 character) | A dot-path into the posted payload, e.g. customer.phone reaches payload["customer"]["phone"], and items.0.sku reaches the sku key of the first element of an items array. |
|
operator |
string | No | "equals" |
One of the twelve values in the table below. |
value |
any JSON type | No | null |
The value to compare field against. Ignored by exists/not_exists, which only check presence. |
operator is one of:
| Operator | Checks |
|---|---|
equals |
Exact value match |
not_equals |
Value differs |
contains |
Substring or list membership |
not_contains |
Substring or list membership is absent |
exists |
The field is present in payload at all |
not_exists |
The field is absent from payload |
gt |
Numeric greater-than |
gte |
Numeric greater-than-or-equal |
lt |
Numeric less-than |
lte |
Numeric less-than-or-equal |
in |
Value is one of a given set |
not_in |
Value is not one of a given set |
regex |
Value matches a regular expression |
See Triggers & events for worked prose examples of these operators against a sample payload.
Three validation rules are enforced on create, each returning 422 with a
message explaining which field is missing:
- An
action_typeof"whatsapp"or"both"withoutwhatsapp_template_sidset is rejected. - An
action_typeof"telegram"withouttelegram_message_templateset is rejected. Telegram has no pre-approved template to fall back on, so without a body there is nothing to send. - An
action_typeof"agent_task"withoutcontext_templateset is rejected, sincecontext_templatedoubles as the task instruction and there's nothing to run without it.
curl -X POST https://api.your-domain.com/api/v1/agents/1/triggers \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{
"name": "Abandoned cart follow-up",
"event_type_filter": "abandoned_cart",
"conditions": [
{"field": "cart_value", "operator": "gt", "value": 50}
],
"condition_logic": "all",
"phone_field_path": "customer.phone",
"context_template": "You are calling {{ customer.name }} about their cart worth ${{ cart_value }}, left at {{ items | length }} items. Offer help completing checkout.",
"cooldown_seconds": 3600
}'
import httpx
response = httpx.post(
"https://api.your-domain.com/api/v1/agents/1/triggers",
headers={"Authorization": f"Bearer {admin_token}"},
json={
"name": "Abandoned cart follow-up",
"event_type_filter": "abandoned_cart",
"conditions": [
{"field": "cart_value", "operator": "gt", "value": 50},
],
"condition_logic": "all",
"phone_field_path": "customer.phone",
"context_template": (
"You are calling {{ customer.name }} about their cart worth "
"${{ cart_value }}, left at {{ items | length }} items. "
"Offer help completing checkout."
),
"cooldown_seconds": 3600,
},
)
response.raise_for_status()
trigger = response.json()
const response = await fetch(
"https://api.your-domain.com/api/v1/agents/1/triggers",
{
method: "POST",
headers: {
Authorization: `Bearer ${adminToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Abandoned cart follow-up",
event_type_filter: "abandoned_cart",
conditions: [{ field: "cart_value", operator: "gt", value: 50 }],
condition_logic: "all",
phone_field_path: "customer.phone",
context_template:
"You are calling {{ customer.name }} about their cart worth ${{ cart_value }}, left at {{ items | length }} items. Offer help completing checkout.",
cooldown_seconds: 3600,
}),
},
);
const trigger = await response.json();
Returns 201 Created with the full Trigger, including its new id:
{
"id": 4,
"agent_id": 1,
"name": "Abandoned cart follow-up",
"enabled": true,
"event_type_filter": "abandoned_cart",
"conditions": [{ "field": "cart_value", "operator": "gt", "value": 50 }],
"condition_logic": "all",
"phone_field_path": "customer.phone",
"from_number": null,
"context_template": "You are calling {{ customer.name }} about their cart worth ${{ cart_value }}, left at {{ items | length }} items. Offer help completing checkout.",
"cooldown_seconds": 3600,
"action_type": "call",
"whatsapp_template_sid": null,
"whatsapp_template_variables_template": null
}
List Triggers¶
Returns every Trigger on the agent, enabled and disabled, ordered by
id, with no pagination.
Get a single Trigger¶
Returns 404 if trigger_id doesn't exist, or belongs to a different
agent than the one in the URL.
Update a Trigger¶
Every field is optional; send only what you want to change. Unlike
create, there's no cross-field validation on update. You could, for
example, PATCH a whatsapp_template_sid onto a Trigger and only switch
its action_type to "whatsapp" in a later call, and neither individual
request would be rejected.
const response = await fetch(
"https://api.your-domain.com/api/v1/agents/1/triggers/4",
{
method: "PATCH",
headers: {
Authorization: `Bearer ${adminToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ cooldown_seconds: 7200, enabled: false }),
},
);
const trigger = await response.json();
Returns the full, updated Trigger.
Delete a Trigger¶
Returns 204 No Content. Deletion is immediate and permanent: a future
event that would have matched this Trigger simply won't anymore. There's
no soft-delete or recovery.
Dry-run a Trigger: /test¶
Evaluates a sample payload against an already-created Trigger exactly
the way real event ingestion would, condition matching, phone resolution,
context_template rendering, all of it, but stops short of any live
effect: no call is queued, no WhatsApp message is sent, no agent_task
runs, and no cooldown window is recorded or checked. Use it to validate a
Trigger's configuration before pointing a real integration at
Sending events.
Request¶
| Field | Type | Required | Description |
|---|---|---|---|
payload |
object | Yes | A sample event payload, same shape as what you'd post to the events endpoint. |
Response¶
| Field | Type | Description |
|---|---|---|
matched |
boolean | Whether conditions matched this payload under condition_logic. |
resolved_phone |
string or null | The phone number resolved from phone_field_path, if matched and one was found. Always null for an agent_task Trigger, there's no phone to resolve. |
phone_valid |
boolean | true only if matched and resolved_phone is non-null. Always false for agent_task. |
rendered_context |
string or null | context_template rendered against payload, if matched. For agent_task, this is the task instruction the agent would actually execute. |
import httpx
response = httpx.post(
"https://api.your-domain.com/api/v1/agents/1/triggers/4/test",
headers={"Authorization": f"Bearer {admin_token}"},
json={
"payload": {
"customer": {"name": "Prince", "phone": "+15557654321"},
"cart_value": 89,
"items": [1, 2, 3],
},
},
)
response.raise_for_status()
result = response.json()
const response = await fetch(
"https://api.your-domain.com/api/v1/agents/1/triggers/4/test",
{
method: "POST",
headers: {
Authorization: `Bearer ${adminToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
payload: {
customer: { name: "Prince", phone: "+15557654321" },
cart_value: 89,
items: [1, 2, 3],
},
}),
},
);
const result = await response.json();
{
"matched": true,
"resolved_phone": "+15557654321",
"phone_valid": true,
"rendered_context": "You are calling Prince about their cart worth $89, left at 3 items. Offer help completing checkout."
}
A payload that doesn't satisfy conditions returns matched: false with
every other field null or false. No error is raised: this is the
expected shape for "this Trigger wouldn't have fired," not a failure
response.
End-to-end: create, test, then fire for real¶
Putting the pieces together, using the abandoned-cart Trigger created above:
- Create the Trigger (already done above; its
idis4). - Test it against a representative payload, confirming
matched: trueand a sensiblerendered_context, without placing a real call (also already done above). - Fire it for real by posting an actual event. This is where
Sending events takes over: the same
POST /api/v1/agents/{agent_id}/eventscall, authenticated with anevents:write-scoped API key rather than your admin token, with a payload shaped to match:
curl -X POST https://api.your-domain.com/api/v1/agents/1/events \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"event_type": "abandoned_cart",
"payload": {
"customer": { "name": "Prince", "phone": "+15557654321" },
"cart_value": 89,
"items": [1, 2, 3]
}
}'
A successful match here returns {"matched": true, "call_ids": [...]},
and moments later an outbound call actually rings +15557654321 with
the agent opening on the rendered context. Track it via
Calls or the event log.
Next¶
- Sending events: the endpoint that actually evaluates Triggers against live traffic.
- Event log: read back what happened to an event after the fact, including why it didn't match.
- Calls: the call a matching
callorbothTrigger queues. - Audit log: every create, update, and delete on this page's endpoints is recorded here.