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.
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.

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.
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.

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 alert 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.