Automation: rules, macros and assignment
Rules run on events, on SLA breaches or after a conversation has waited long enough, check conditions and run actions in a set order, with a run log, a per-rule budget and loop protection. Macros are the same action list applied by hand, webhooks included. Assignment routes conversations by team or inbox, skills and load. Everything is available through the API.
Last updated
Automation is one area of the app (the Automation item in the rail; on a phone, through More) with tabs that switch in place: Rules, the Execution log, Macros, Assignment and Chatbot flows. Rules, macros and the log need automations:manage, flows flows:manage, and Assignment any of teams:manage, inboxes:manage or agents:manage; a tab you may not use is hidden. Who is in a team stays under Settings › Agents and teams.
A rule
Trigger, conditions (the same condition tree as views), actions, and either a delay (event triggers) or a threshold (time triggers). A rule runs with the permissions and visibility of the person who last saved it: an action they may not perform is skipped and logged, and a conversation they cannot see is skipped. A rule's reply or note cannot use a placeholder (such as {{contact.attributes.<key>}}) whose value the author's role sees masked or hidden; saving it answers 422.
curl -X POST "$KOMMIO_URL/automations" -H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" -d '{
"name": "Billing questions to the billing team",
"trigger": "conversation.created",
"conditions": { "any": [
{ "field": "subject", "op": "contains", "value": "invoice" },
{ "field": "subject", "op": "contains", "value": "refund" }
] },
"actions": [
{ "type": "add_tag", "params": { "tag_id": 4 } },
{ "type": "assign_team", "params": { "team_id": 2 } },
{ "type": "set_priority", "params": { "priority": "high" } }
],
"enabled": true
}'await api.POST("/automations", {
body: {
name: "Billing questions to the billing team",
trigger: "conversation.created",
conditions: { any: [{ field: "subject", op: "contains", value: "invoice" }, { field: "subject", op: "contains", value: "refund" }] },
actions: [{ type: "add_tag", params: { tag_id: 4 } }, { type: "assign_team", params: { team_id: 2 } }, { type: "set_priority", params: { priority: "high" } }],
enabled: true,
},
});Triggers
| Trigger | Fires |
|---|---|
conversation.created | A conversation starts on any channel. |
conversation.updated | Status, priority, assignee, team, tags or attributes change. |
conversation.resolved | The status moves to a resolved category. |
conversation.reopened | A resolved or closed conversation is open again, however it happened: the customer wrote back, an agent or the API reopened it, or a rule did (that one reaches webhooks but runs no rules, like every change a rule makes). Not on creation, not between open, pending and snoozed, not when a snooze ends, and never for imports. Webhooks can subscribe to it too. |
message.created | Any message, including the customer's replies. Messages written by rules never trigger rules. |
conversation.handoff | A bot handed the conversation to people. |
csat.submitted | A customer answered or changed a satisfaction survey. Conditions can use csat.rating (1 to 5), csat.has_comment and csat.comment. |
conversation.sla_breached | A conversation passed a target of the SLA policy that applies to it while still waiting: no first response yet, or not resolved. Once per conversation and target, from the minute check; the condition sla.kind (first_response or resolution) tells them apart. A rule's own changes never make it fire again. |
time.customer_waiting | The customer's latest reply has been unanswered for the threshold (open, and the last reply is the customer's). |
time.no_customer_reply | The team replied last and the customer has not answered for the threshold (open or pending). |
time.open_for | The conversation has been neither resolved nor snoozed for the threshold since it was created. |
Time triggers
Time triggers wait for a state rather than an event. Set threshold_seconds between 60 (one minute) and 7776000 (90 days); delay_seconds must be 0. With business_time: true only the workspace's business hours count, in the schedule's time zone and across daylight-saving changes.
- A rule fires at most once per conversation per episode. For "customer waiting" a new customer message or an answer starts a new episode; for "no customer reply" only a customer answer does, so an automatic nudge never loops; "open for" fires once per conversation.
- A rule acts only on thresholds crossed after it was armed (switched on, or its trigger or threshold changed), so saving a rule never fires on the existing backlog.
- Conversations are checked every minute, and the conditions are checked again right before the rule runs.
- Next due. The rule's card and the editor show when the rule fires next: the earliest conversation still waiting for it (an episode started since the rule was armed and not yet queued) and the moment it crosses the threshold, in business hours when the rule counts them. A moment already passed reads "due at the next check"; a rule that is off, or has nothing waiting, shows nothing. It is computed the way the minute check works, so it is what the check will do.
GET /automationscarries it on each enabled time rule asnext_due_atandnext_due_conversation_id.
# nudge the customer after 2 business hours without an answer
curl -X POST "$KOMMIO_URL/automations" -H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" -d '{
"name": "Nudge after 2 business hours", "trigger": "time.no_customer_reply",
"threshold_seconds": 7200, "business_time": true,
"conditions": { "all": [] },
"actions": [ { "type": "reply", "params": { "content": "Still there? Reply whenever you are ready and we will pick this up." } } ]
}'
# after-hours: tag chats that arrive while the team is away
curl -X POST "$KOMMIO_URL/automations" -H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" -d '{
"name": "Tag after-hours chats", "trigger": "conversation.created",
"conditions": { "all": [ { "field": "business_hours_open", "op": "eq", "value": false } ] },
"actions": [ { "type": "add_tag", "params": { "tag_id": 7 } } ]
}'The condition business_hours_open ("Within business hours") is true when the conversation's inbox is open now: the inbox's own schedule when it has one, otherwise the workspace's. It works with every trigger.
Actions
| Type | Params | Needs |
|---|---|---|
assign_agent | user_id | conversations:assign |
assign_team | team_id | conversations:assign |
unassign | conversations:assign | |
unassign_team (Remove team) | conversations:assign | |
require_skills (Require skills) | skills, comma-separated, such as billing, spanish | conversations:assign |
assign_bot | bot_id | conversations:assign |
assign_flow (Assign to chatbot flow) | flow_id | conversations:assign |
assign_ai_agent (Assign to AI agent) | conversations:assign | |
add_tag, remove_tag | tag_id | conversations:manage |
tag_channel (Tag with the channel) | conversations:manage | |
set_status | status (key) | conversations:manage |
set_priority | priority | conversations:manage |
snooze | hours (1 to 720) | conversations:manage |
set_attribute (Set a custom attribute) | attribute, value (empty clears it) | conversations:manage |
flag_attachments (Mark the files as personal data) | sensitivity: pii.identity_doc or payment | conversations:manage |
reply | content | conversations:reply |
note | content | messages:write_private |
run_connector (Run a data connector) | connector_id | connectors:manage |
send_webhook (rules and macros) | url, payload (full or minimal), secret | webhooks:manage to save it |
plugin:<key> | as declared by a core module of a self-hosted server | conversations:manage |
GET /automations returns the rules together with the trigger list and this action catalogue (actions), with a JSON Schema per action, which is what the rule editor renders. A rule holds up to 20 actions and runs them in order: an action the author may not perform is skipped, and a failing action stops the rest; both are recorded in the run log.
Require skills replaces the skills the conversation needs from its assignee; assignment then picks only agents holding all of them, together with any the inbox requires. Mark the files as personal data marks every unmarked file of the conversation as an ID document or payment detail, so each role sees it by its field grants; files already marked keep their mark.
Assign to bot
assign_bot gives the conversation to a bot attached to its inbox; the bot receives conversation.assigned. It never takes a conversation from an agent: it is skipped, with the reason in the run log, when the conversation has an assignee, is resolved or closed, is already held by that bot, when the bot is paused, or when the bot is not the inbox's bot.
Send a webhook
send_webhook posts a snapshot of the conversation, built when the rule ran, to any https URL. Saving it needs webhooks:manage, and URLs that resolve to private addresses are refused when the rule is saved and again at delivery. payload is full (the default: ids and state plus subject, contact name, email and phone, tags, custom attributes and the last message preview) or minimal (ids and state only). The optional secret is stored encrypted and never returned (the rule or macro shows has_secret: true); leave it out when you update the rule to keep it. In a macro the step sends when an agent applies it.
Kommio-Event: automation.send_webhook
Kommio-Trigger: csat.submitted
Kommio-Rule: 18
Kommio-Signature: t=1790000000,v1=5f1c7e2a9b4d6f8e0c1a3b5d7f9e2c4a6b8d0f1e3c5a7b9d2f4e6a8c0b1d3f5e
{
"type": "automation.send_webhook",
"account_id": 12,
"rule": { "id": 18, "name": "Escalate low ratings" },
"trigger": { "event_id": 91822, "type": "csat.submitted" },
"occurred_at": "2026-09-22T09:12:00Z",
"conversation": { "id": 4821, "display_id": 231, "inbox_id": 3, "contact_id": 77, "status": "resolved", "priority": "none",
"assignee_id": 7, "subject": "Order 4242", "contact": { "id": 77, "name": "Ana", "email": "[email protected]" },
"tags": ["billing"], "custom_attributes": {}, "last_message": "Thanks", "created_at": "2026-09-22T08:40:00Z" }
}Kommio-Signature is sent when the action has a secret and is verified exactly like a webhook signature. Deliveries go through the shared ledger with the webhook retry schedule: GET /automations/{id}/deliveries lists them (the stored body redacted for the reader by their field grants), and POST /automations/{id}/deliveries/{delivery_id}/replay and POST /automations/{id}/deliveries/replay-failed send them again. A replay sends the stored snapshot. The full payload carries the contact's email, phone and attributes in clear, as a bot's payload does, because the workspace chose the receiver.
Order, budgets, delays, testing and the run log
Rules with the same trigger run top to bottom. Reorder them by dragging on the Rules tab, or send every rule id of the workspace in the new order (a list that no longer matches the workspace answers 409; reload and try again):
curl -X PUT "$KOMMIO_URL/automations/order" -H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" \
-d '{"ids": [12, 18, 9, 4]}'
# follow up two hours after a customer message, if the conversation is still open
curl -X POST "$KOMMIO_URL/automations" -H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" -d '{
"name": "Flag if still open", "trigger": "message.created", "delay_seconds": 7200, "max_runs_per_hour": 5,
"conditions": { "all": [ { "field": "message.direction", "op": "eq", "value": "inbound" }, { "field": "status_category", "op": "eq", "value": "open" } ] },
"actions": [ { "type": "set_priority", "params": { "priority": "high" } } ]
}'
# dry run: would rule 12 match conversation 4821 right now?
curl "$KOMMIO_URL/automations/12/test?conversation_id=4821" -H "Authorization: Bearer $KOMMIO_KEY"
# what it did
curl "$KOMMIO_URL/automations/12/runs" -H "Authorization: Bearer $KOMMIO_KEY"- Budget.
max_runs_per_hour(default 20, 1 to 100) caps how often a rule may act on one conversation in an hour; past it, runs are logged as skipped as a possible loop. This also catches loops through the outside world, such as two auto-responders answering each other. - Loop guard. Changes and messages made by rules never trigger rules.
- Delays.
delay_seconds(event triggers only, up to 7 days) waits, then checks the conditions again, so a customer who answered in the meantime is not nudged. - Run log. Every evaluation that matched, was scheduled, skipped or failed leaves a run with the reason and the outcome of each action; the Execution log tab shows the selected rule's.
Conditions are one condition tree. The editor edits the top level and one level of nested groups, each any of, all of, none of or not all of its conditions (a not around a group, or around a single condition; "+ Not group" starts a none of group); deeper nesting is shown as text and kept as it was on save, so every saved rule round-trips unchanged. A rule's summary in the list names the inbox, team, tag or person it refers to, and shows an id only for one that no longer exists. More rule features are on the roadmap.
Macros
The same action list, applied by an agent from the conversation with one click or from the API, with the permissions of the person applying it: a step on an attribute they see hidden is skipped. Macros live on the Macros tab of Automation.
curl -X POST "$KOMMIO_URL/macros" -H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" -d '{
"name": "Escalate to billing",
"actions": [ { "type": "assign_team", "params": { "team_id": 2 } }, { "type": "set_priority", "params": { "priority": "urgent" } }, { "type": "note", "params": { "content": "Escalated to billing." } } ]
}'
# apply macro 3 to conversation 4821
curl -X POST "$KOMMIO_URL/conversations/4821/macros/3" -H "Authorization: Bearer $KOMMIO_KEY"A macro that sends a webhook
A macro may carry the send_webhook step. Saving it needs webhooks:manage as well as automations:manage, as a rule does; applying it needs only what applying the macro needs, since the agent picks neither the URL nor the secret. There is no event behind a hand-applied macro, so the body names the macro and who applied it, the trigger is macro.applied, and the headers carry Kommio-Macro and Kommio-Trigger: macro.applied. Every application sends again, so applying a macro twice posts twice. Deliveries are listed and replayed under GET /macros/{id}/deliveries like a rule's.
Kommio-Event: automation.send_webhook
Kommio-Trigger: macro.applied
Kommio-Macro: 3
{
"type": "automation.send_webhook",
"account_id": 12,
"macro": { "id": 3, "name": "Escalate to billing" },
"applied_by": 7,
"trigger": { "event_id": 0, "type": "macro.applied" },
"occurred_at": "2026-10-02T14:03:00Z",
"conversation": { "id": 4821, "display_id": 231, "inbox_id": 3, "contact_id": 77, "status": "open", "priority": "urgent",
"assignee_id": 7, "subject": "Order 4242", "contact": { "id": 77, "name": "Ana", "email": "[email protected]" },
"tags": ["billing"], "custom_attributes": {}, "last_message": "Can you check my refund?", "created_at": "2026-10-02T08:40:00Z" }
}Assignment
The Assignment tab gathers everything that decides who gets a conversation: the presence grace period, every team and every inbox as a row saying how it assigns (opened in place to change it), and every agent's skills and own capacity. Auto-assignment listens to events, so it works however the conversation arrived: a channel, the API, a bot's hand-off or a rule that cleared the assignee.
- Where the agent is looked for. A conversation with a team is assigned among the team's members when the team auto-assigns; one without a team among its inbox's agents when the inbox has its own assignment on. A team always wins. Candidates are active members whose availability is Online. A workspace with rules on
conversation.createdwaits a few seconds before assigning, so a rule's team or required skills are seen first. - Skills. Members have skills (tags such as
billingorspanish). A conversation needs the inbox's required skills plus any a rule asked for; only members holding all of them are candidates, and with none it waits. - Capacity. An agent holding their capacity of open and pending conversations is skipped; their own capacity wins over the team's or inbox's, and 0 is unlimited. With everyone full the conversation waits and is assigned when someone frees up and it is touched again.
- Routing. Round robin (the default) takes the next candidate in a fixed order. Balanced by load takes the candidate with the lowest open load against their capacity, so unequal capacities fill in proportion; ties go round robin.
- Availability follows the app. Besides the status an agent chooses, the app sets Away after inactivity, and an agent whose app has been closed for the workspace's grace period (2, 5, 10, 15 or 30 minutes, or never; 5 by default) is set Offline and Online again as soon as it connects. A status the person chose is never changed.
- When an agent goes offline. A team or inbox can keep their conversations (the default), return the ones waiting for a reply to the queue, or reassign them to someone Online with the same routing, after a delay (10 minutes by default). Pending and snoozed conversations, and ones the agent answered last, stay with them; each move leaves an activity line.
# the Billing inbox assigns its own conversations, balanced by load, to agents with the billing skill
curl -X PUT "$KOMMIO_URL/inboxes/3" -H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" -d '{
"name": "Billing",
"assignment": { "auto_assign": true, "routing": "balanced", "capacity_per_agent": 8, "assign_outside_hours": false,
"required_skills": ["billing"], "reassign_offline": "queue", "reassign_after_minutes": 10 }
}'
# Dana's skills and her own capacity (0 uses the team's or inbox's)
curl -X PUT "$KOMMIO_URL/account/members/7/routing" -H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" \
-d '{"skills": ["billing", "spanish"], "capacity": 5}'assignment: null turns an inbox's own assignment off; a team carries the same fields at its top level on PUT /teams/{id}.
Reports › Assignment shows, per agent, conversations assigned, reassigned away, the time to first assignment and the average open load, each opening the conversations behind it (reports).
Every claim on these pages is covered by a test in the repository, and the API reference is generated from the same contract every server serves at /api/openapi. Corrections to [email protected].