This page describes
ravi.bot@next. On @latest, --session only accepts main or isolated (default isolated), so the Sessions section does not apply there.How a trigger works
Ravi publishes events on its event bus (NATS,nats://127.0.0.1:4222 by default): reactions, Slack button clicks, CLI commands, permission denials, Console inbox items and more. A trigger subscribes to a topic pattern. For each event, the daemon:
- skips events produced by trigger turns (see Anti-loop);
- resolves the session name, if it is a template;
- checks the cooldown, and drops the event if the trigger is cooling down (skipped when the cooldown is
offor the trigger is debounced); - evaluates the filter, and drops the event if it does not match;
- drops an exact repeat of an event it already handled in the last 60 seconds;
- fires: sends a prompt to an agent session (
--message) or runs a command (--shell). A debounced trigger instead adds the event to the current burst and fires once when the burst goes quiet (see Debounce).
Create a trigger
1
Pick a topic
add command.2
Add the trigger
3
Check that it is active
State: active means the trigger is enabled and valid, so the running daemon subscribes to it. Other states are listed in Manage triggers.4
Fire it with a fake event
{"_test": true, "message": "Test event fired via CLI", "timestamp": "..."} on the trigger’s topic. It skips the cooldown and the filter, and a session template fills missing values with test. It runs the real agent or the real shell command.Options
Topics
ravi triggers topics (or --json) is the catalog of topics Ravi publishes. The main families:
The catalog is a guide, not a whitelist. A trigger can subscribe to any subject your own code publishes; the CLI only warns when a topic is not in the catalog or looks like a channel alias (such as
whatsapp.*.reaction; use ravi.inbound.reaction). The Events reference lists payloads.
ravi.inbound.reaction identifies the reacted message only by targetMessageId. If the reaction must resolve to a business item, store a mapping from the outbound message id to your item when you send the message.
Filters
A filter compares fields of the event data. Without a filter, every event on the topic fires.
Combine with
&&, ||, ! and parentheses. ! binds tightest, then &&, then ||.
- Values are quoted strings (single or double quotes). The field is compared as text:
truebecomes"true",42becomes"42", a list becomes its items joined by commas. - There is no
<,>or numeric comparison. The CLI rejects them. - A missing field is false, even with
!=.data.payload.rowId != "x"does not match an event that has norowId. - Path segments are names. You cannot index a list in a filter (templates can, see below).
addandsetreject invalid filters. A stored filter that does not compile never fires: the trigger showsState: invalid_filterandravi triggers show <id>prints the error.
Message templates
--message can use placeholders resolved from the event:
{{topic}}: the topic that fired.{{data.<path>}}: any field of the event data. Lists take an index:{{data.payload.row.values.topic_id.0}}.
What the agent receives
Data: block is the whole event as JSON. When a trigger uses a topic’s default template instead of your own --message, the Data: block is left out.
Sessions
--session names the session the trigger runs in.
Template rules:
- The resolved name is normalized: lowercase, anything other than letters and digits becomes
-, at most 64 characters (longer names end in a hash). - If a placeholder is missing or empty, the event is skipped and logged. It is never sent to another session.
- A template never runs in another agent’s session: if the resolved name belongs to one, the event is skipped. A literal name can point at any existing session.
ravi triggers set <id> session <name>.
Where the answer goes
The trigger picks a chat for the answer in this order:- The chat where the reply session last talked.
--reply-sessionsets it; when the agent creates the trigger during a chat, it is set to the agent’s own session automatically. - Only when there is no reply session and no frozen creator chat: the chat where the target session last talked.
- The creator’s chat, recorded when the trigger was created. Clearing the reply session does not clear it.
Response target unresolved — dropping emit. In practice:
- No
--session, created by the agent in a chat: the trigger runs in that chat’s session and answers in that chat. This is the simplest setup. - No
--session, created from your terminal: the trigger runs in the agent’s main session and answers where that session last talked. --session mainwith a reply session: the trigger runs in the reply session and answers in its chat.- Any other named session (for example a queue worker): it answers in a chat only if that session already owns one and either the reply session is that same session (
--reply-session <same-name>) or the trigger has no reply session and no creator chat (created from your terminal). A session the trigger creates owns no chat, so its answers stay in it. isolated: never answers in a chat.
ravi sessions read <name>. If someone must be told, say so in the message, for example: Then report with: ravi sessions inform <owner-session> "<summary>".
Change the reply session with ravi triggers set <id> replySession <name> (- clears it).
Cooldown
- Default
5s. The value is a positive integer and a unit:s,m,hord.off(or0s) disables the cooldown: every event that passes the filter fires. - The cooldown is checked before the filter. An event that arrives during the cooldown is dropped, not queued.
- The clock starts only when an event passes the filter. Events the filter rejects do not use up the cooldown.
- With a named or templated session, the cooldown counts per resolved session. With
mainorisolated, it counts for the whole trigger. - Separately, Ravi fires a trigger at most once for the same event within 60 seconds (same
dedupeKey,eventIdor message id, or an identical payload).
off, use debounce to absorb bursts without dropping anything, or add a cron job that runs the same prompt as a sweep.
Debounce
--debounce <duration> turns a burst of events into one fire. Each event that passes the filter joins the burst and restarts the quiet window; the trigger fires once the window passes with no new event.
- The fire carries every event of the burst, oldest first. An agent trigger gets them in the prompt (
Batch: N events…and anEvents:list); a shell trigger gets them in$RAVI_TRIGGER_EVENTS_FILE, with the count in$RAVI_TRIGGER_EVENT_COUNT. The message template and$RAVI_TRIGGER_DATA_FILEuse the burst’s last event. - A constant stream still fires: a burst fires at most 10× the window after its first event.
- A burst keeps its last 200 events; older ones are counted as omitted.
- With a named or templated session, each resolved session has its own burst.
- The cooldown, if set, applies between fires: a burst that is due while the trigger is cooling down waits for it instead of being dropped.
- Bursts live in the daemon’s memory. Events still waiting when the daemon stops are lost (logged at
warn). ravi triggers set <id> debounce <duration|off>changes it.
Shell triggers
A shell trigger runs a command for each event, with no agent and no tokens:- Read the event from
$RAVI_TRIGGER_DATA_FILE. Do not paste event text into the command. - The command runs in the daemon’s working directory and environment. Use absolute paths everywhere, including inside the script.
--timeoutdefaults to 10 minutes.--env-fileaddsKEY=valuelines to the environment.- Output goes to the daemon log. On a non-zero exit or a timeout,
--on-error notify-session:<session>sends that session a[System] Inform:message with the command, exit code, stderr and stdout. - A script has no chat of its own. Commands in it that send messages need an explicit account and destination.
ravi triggers set <id> message "..." turns a shell trigger into an agent trigger, and set <id> shell "..." does the reverse.
Anti-loop
- Topics under
ravi.session.*are internal. The CLI accepts them, but the daemon never subscribes (State: blocked_topic). - Events produced by trigger turns are skipped: topics that contain
:trigger:, and events marked_trigger: trueor with a trigger turn origin. - That does not cover everything. When an agent writes to a base, its own write comes back as a Bases event. Filter it out with
data.actor.type == "user"(see below). - Cooldown and duplicate checks limit bursts. They are not loop protection.
React to Bases rows
Pages backed by Bases can wake an agent when someone creates or edits a row.1
Deliver the base's row events to this installation
--events (for example row.created,approval.requested), --props a,b or --view <view-id>. Items of a filtered subscription carry payload.subscriptionId.2
Know the event
Each row change arrives on
ravi.console.inbox.item with category: "bases". Trimmed example:eventTypeisbases.row.created,bases.row.updated,bases.row.archivedorbases.row.restored. A request that changes more than 50 rows sends onebases.rows.bulk_changedinstead, withoutrowIdand withoutrow.- Approval columns add
bases.approval.requestedandbases.approval.decided(withpayload.propertyIdandpayload.state). Agent columns addbases.agent.requested(withpayload.propertyIdandpayload.inputsHash) andbases.agent.backfill(one item withrowCount, norowId, when the column is added to a base with rows). actor.typeisuserfor people (on a page or in the Console) andclifor every CLI command and agent on this installation;actor.idis the accountable person.payload.surfaceispage,console,cliorsystem.payload.rowholds the row values read at delivery (no body), on every event about one row. If that read fails,rowEnrichment.statusisfailedandrowis missing, so filters on values are false and session templates on values skip the event.- Select and status values are option ids, not labels. List them with
ravi bases props list requests --json.
3
Add the trigger
actor.type == "user" keeps the agent’s own updates from waking it again. Use data.payload.surface == "page" instead if only the page should count. To run one session per item, use a template such as --session 'forum-{{data.payload.row.values.topic_id.0}}'.Shortcut: ravi bases subscribe requests --events row.created --trigger --agent main --session requests-queue --message '...' subscribes and creates the trigger with the subscription’s filter in one command. Its filter also lets through bases.rows.bulk_changed when you subscribe a row.* event and bases.agent.backfill when you subscribe agent.requested, because a bulk write sends those instead. It does not add the actor condition. When the agent writes the same base, copy the generated filter from the command’s JSON (trigger.filter) and set it again with the condition appended: ravi triggers set <id> filter '<generated filter> && data.actor.type == "user"'. triggers set replaces the whole filter, so setting only the actor condition would drop the base, subscription and event predicates. From your terminal the trigger starts enabled. When an agent runs it during a conversation, the trigger starts disabled and the output names the command that turns it on (ravi triggers enable <id>).4
Test end to end
Manage triggers
ravi triggers set <id> <key> <value> accepts: name, message, shell, exec, timeout, env-file, on-error, topic, agent, account, session, cooldown, debounce, filter, replySession. For filter, agent, account, replySession, timeout, env-file and on-error, pass - to clear the value.
Debug a trigger
It never fires
It never fires
ravi triggers show <id>: checkState.Filter rejectscounts events that arrived but did not match; withFire count: 0and rejects above zero, your filter paths do not match the real event.ravi daemon logs -f: the first rejected event logsTrigger filter rejected an eventwith the event’s top-level keys.- Watch the topic yourself (needs the
natsCLI):nats sub "ravi.audit.denied" --server nats://127.0.0.1:4222. - A session template that does not resolve logs
Skipping trigger fire; session name template did not resolve.
It fires, but no message shows up
It fires, but no message shows up
ravi sessions read <session>: the answer may be in the session.ravi daemon logs -f:Response target unresolved — dropping emitmeans the session that ran the turn has no chat attached. See Where the answer goes.- The agent may have answered
@@SILENT@@.
Force the daemon to reload
Force the daemon to reload
Every
add, set, enable, disable and rm reloads the subscriptions. To force it, run ravi triggers disable <id> and then ravi triggers enable <id>.Related
- Cron jobs: run the same prompt on a schedule, as a sweep.
- Interactive pages with Bases: the pages that produce row events.
- Events: topic families and payloads.