Skip to content

Chat triggers

This page lists what a message in a bound channel does, the fields of a botMessage trigger, and the decisions, notes and limits of a trigger's threads. The fields' types are in the Binding reference.

What a message does

These are the defaults, which a Binding with no chat.triggers gets.

Message What happens
A mention in a bound channel, not in a thread A new thread and a new session.
A reply in a thread Agent Kourier owns Continues that session. With chat.threadReplies: mention, only a reply that mentions Agent Kourier does; the rest are ignored, except a typed answer to a pending question that takes text replies.
A mention inside a thread Agent Kourier does not own, in a bound channel A new session anchored to that thread, which Agent Kourier owns from then on.
A reply without a mention in any other thread Ignored.
Any message Agent Kourier posted, including the edit events of its own posts Ignored, always.
A message from any other bot or app Ignored, unless a botMessage trigger names its bot ID.
A mention of Agent Kourier by any other bot or app A turn in the thread that holds it, within fixed limits, unless the Binding says otherwise. See A bot's mention.
An edit or deletion of any message, or another non-message subtype Ignored. Plain messages, thread_broadcast and file_share (its text only) count as messages.
A direct message Not served yet.

The trigger list

  • An empty chat.triggers is [{type: mention}], the table above.
  • A list that names triggers is the whole list. A Binding that lists only botMessage triggers starts no session from a mention, in or out of a thread.
  • Triggers are tried in order, and the first that matches handles the message. A trigger's index in the list names it in logs and metrics.
  • v1 has two types: mention and botMessage. A mention trigger takes no other field.
  • Whether a reply in an owned thread needs a mention is chat.threadReplies, not a trigger.

A bot's mention

Any bot or incoming webhook that can post in a bound channel can start a turn by mentioning Agent Kourier, because Slack cannot tell an agent from any other bot. This is on by default for a Binding with a mention trigger, including one with no chat.triggers. Set chat.botMentions: none to turn it off. A Binding whose list leaves out the mention trigger takes no bot's mention either.

Where the turn goes The thread that holds the mention, or a new thread for a mention outside one. A new session if Agent Kourier does not own the thread.
Limits Fixed, and across every bot on the Binding: 3 turns in any 30 minutes, 1 at a time, 20 in any 24 hours. They count mentions, including those in a thread Agent Kourier already owns. "1 at a time" lasts while the mention's own turn is queued or running, including while it waits for a person's answer: a later turn in its thread does not hold it. A mention whose session takes no turn does not count. A mention in an alert's thread does not count as that alert running under the trigger's maxConcurrent.
The prompt Fixed, with the post and the thread so far as untrusted data. The post is its text, its first attachment's fields and, if that attachment has no text, what its section, context, header and rich text blocks say past its text. The thread is what opened it and every author's messages since, except Agent Kourier's own and what were already turns, at most 50: what opened it stays, with the newest. A bot's message in the thread shows the same parts as the post, and its broadcasts and file shares (their text and blocks) count. A later mention gets what came since the previous bot's mention, so a reply sent to the agent as context on a person's turn can come again.
Replies in the thread A session that a bot's mention began takes a person's reply as a turn only if it mentions Agent Kourier, whatever chat.threadReplies says, and the final message says so. A thread Agent Kourier already owned keeps its Binding's rule.
Rating buttons As on an answer to a person: only with feedback.enabled, and counted as an answer.
A limit is reached No turn and nothing in the thread. Counted, audited, and in the digest line of its window.

These are never taken as a mention:

  • Agent Kourier's own posts.
  • An edit that adds a mention.
  • A bot that a botMessage trigger of the Binding names. That trigger reads its posts.
  • Any bot's mention in a channel shared with another workspace.

A session past its threadTTL, or whose Binding has left the config, takes no turn from a bot, with no note, and the mention uses none of the limits. A bot's turn never answers a pending question; it stays a turn of its own, queued until the pause ends, and the question still times out at its deadline. Let another agent start yours has what this does not bound.

botMessage fields

A botMessage trigger works for any bot, named by its sender ID, and is written in full: match, extract, thread, limits and promptTemplate are all in the Binding. A worked trigger for Alertmanager's Slack receiver, with a comment on each regex, is in Investigate Alertmanager alerts.

Field Meaning
from.id The one sender whose messages the trigger accepts, by the platform's ID for it (Slack: the bot ID, B...), never by display name. Agent Kourier's own messages are never handed on, even if a trigger names its ID; that mistake is logged once, at warn.
from.kind What kind of sender from.id names. bot is the only value, and the default. A message matches only when its sender's ID and kind both equal from.id and from.kind; on Slack every bot sender is of kind bot.
match Message field to regex. Every entry must match.
exclude Message field to regex. No entry may match.
extract Named values taken from a field by a regex whose named group has the entry's name. A message in which an extract does not match does not match the trigger.
thread.by key: messages with the same extracted key share one thread, anchored to the first.
thread.cooldown 1m to 168h. No new turn for a key inside the window, which runs from the message that started the turn. Required with thread.by.
thread.closeWhen Extracted field to value, for example {state: RESOLVED}. A matching message posts a note and starts no turn.
thread.maxIdle At least twice the cooldown, at most 720h; unset, the larger of 24h and twice the cooldown. An open thread with no message of its key for this long is abandoned, and the next fire starts a new investigation. It must be longer than the sender's repeat interval.
promptTemplate A Go template over the trigger's view of the message (below). Parsed and run once at load against a placeholder view, so a mistyped field rejects the Binding. It may use text, if, with, the fields below and the functions and, eq, ge, gt, index, le, len, lt, ne, not, or and slice; a loop, a variable, a defined template or any other function rejects the Binding. At most 8192 bytes.
limits.rateLimit Required. maxRuns starts in any per (10s to 24h), and maxConcurrent investigations at once; all three are set.
limits.maxRunsPerDay Required. Starts in any 24 hours. An explicit 0 is no cap.

A botMessage trigger without limits.rateLimit or without limits.maxRunsPerDay does not load: the Binding is rejected with a message that names the missing field. Leaving maxRunsPerDay out is not the same as 0.

Message fields a regex can read: text, title, link, fallback, footer and body. text is the message's top-level text, outside any attachment or card. title, link, body, footer and fallback are the heading, the heading's URL, the main text, the small print and the plain-text summary of the message's structured part. Each platform adapter flattens its native message into them. The Slack adapter reads the message's first legacy attachment: its title is title, its title_link is link, its text is body, its footer is footer and its fallback is fallback. All are raw, as delivered. Later attachments are not read. A field the message lacks is empty.

Regexes are RE2 (linear time), at most 512 bytes, and compile at load, as do the named groups and the template. A Binding that fails any of them is rejected. A regex is also held to the size of the program it compiles to: at most 2000 instructions each, and 5000 for all the regexes of a Binding (see Limits), because the time to try a regex on a message grows with that size and the message's length. A regex reads the first 16 KiB of its field and nothing past it. The check ends at the first regex it refuses.

What a matched message does to its thread

For a trigger with thread.by: key. A close is a message whose extracted fields all equal closeWhen; any other match is a fire.

Message The key has Decision What happens
fire no thread, a closed one past its cooldown, one whose turn never started and is over a minute old, or an open one idle past maxIdle started A new investigation in a thread anchored to this message. The cooldown begins once its turn is taken.
fire an open thread that is not idle past maxIdle, or a closed one inside its cooldown repeat "Fired again at <time>." in that thread, which reopens. No turn.
fire this very message as its thread (a redelivery) dedup Nothing, if its turn started.
fire, key empty empty_key An investigation of its own, with no cooldown and no note.
close an open thread whose turn started closed "Resolved at <time>." in that thread, which closes. No turn.
close no thread, a closed one, or one whose turn never started close_unknown Ignored and counted.
fire would be started, and a limit is reached limited No turn and nothing in the alert's thread. Counted, and in the window's digest line.
fire the first prompt failed its check or could not be rendered not_started "Not investigated: ..." in the thread, an audit entry, and a metric. No cooldown.

A trigger with no thread has none of this: every match starts a turn. The notes are fixed text and a time, never any of the message.

Limits and the digest line

Only a start is limited: a repeat, a close and a note never are. Rate and daily counts are sliding windows that survive a restart. Concurrency counts the trigger's investigations, started in the last 24 hours, that have an active task (running, or paused for a person) or a queued turn. When several limits are reached, the daily cap is reported first, then the rate, then the concurrency.

Once for each window and trigger, one line at the top level of the channel counts the starts a limit stopped, for example "7 bot messages not investigated in the last 10m (rate limit)." A window opens with the first stopped start and lasts the trigger's rateLimit.per. The line holds a count, a length and a limit, never any text of an alert.

The first turn's prompt

The template sees:

Field Value
.Channel The channel the message arrived in.
.State The extracted state, empty if none.
.Key The extracted key.
.Fields Every extracted value, by name.
.Link The platform's permalink to the triggering message (Slack: the message permalink), built by Agent Kourier, never a field of the message. Empty when the platform makes none or cannot make one.
.Source botMessage.

.Fields, .State and .Key come out of the sender's message, so each value the template reads is checked before it reaches the instruction part: the alert-name shape (ASCII letters, digits and _.-:, at most 128 bytes) for fields, state, channel and source; the same plus / for the key; and a plain http or https URL of at most 256 bytes for the link. A value that fails refuses the turn (not_started). A key that holds spaces, such as the Alertmanager example's, fails this check, so a template should read .Fields entries, not .Key.

After the rendered template, the whole message follows, normalized and with its secrets redacted, inside a block labelled as untrusted data.