react-to-events
Skill0 starsChanged 57 days agoMITView on GitHub
Make something happen automatically when a message arrives — from one person, in one group, with a keyword, on one channel, or matching an intent. Covers notifying the owner, flagging a thread for a human, filing a task, and waking a Claude Code session. Use for "notify me when", "react automatically", "watch this chat", "tell me if X writes", "run this on every new lead", or when tempted to poll on a timer.
- Reads credentials
In this repository7 other files it gives its agents.
- connect-channelSkill
- create-agentSkill
- first-messageSkill
- lead-reportSkill
- setup-livechat-widgetSkill
- tasks-boardSkill
- voice-agentSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 21
never this sender, anywhere, calls included → blocked_sender_ids with a phone number, username or address.
- Very specific: line 152
Check first: Call workspace.desktops. If this machine is not in that list, nothing in this section can reach you — use step 2's options …
- Distinctive: line 169
only messages from the other side wake it: what the connected account sends itself is outgoing.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
React when something happens
Nothing here needs a polling loop. The workspace already knows when a message arrives. The work is to say WHICH messages, and WHAT should happen.
Steps
Narrow the event before anything else. "Notify me about messages" is the request; it is never the rule. Ask which person, which group, which words, which channel — and write the answer down as conditions:
- one person's chat → the
thread_idsparameter, after finding the chat withsearch.threads(orcontacts.findfor the person, whose profile names their threads). Its opposite isexcluded_thread_ids, and an exclusion always wins. - never this sender, anywhere, calls included →
blocked_sender_idswith a phone number, username or address. It beats every allow-list. - a group → context_types of dm, group, channel or livechat, plus group_mode set to mentions_only or questions. Without group_mode an agent in a busy chat answers everything.
- words → keywords with keyword_match any or all. Free and deterministic: no model runs when it does not match.
- meaning, when the words vary → ai_filter_ids. The one narrowing that
costs money: the router embeds EVERY candidate message to compare it
with the filter, whether or not anything ends up firing. Reach for it
only when the wording genuinely varies, and pair it with a cheap
condition (a channel, a folder, a chat type) so the paid comparison runs
on a fraction of the traffic. Build and tune the filter with
ai_filters.createandai_filters.testfirst, and reuse an existing one by id instead of making a near-duplicate. - where it came from → channel_types, channel_account_ids, folder_ids, ai_tag_ids. A folder or a tag is how "only leads" is said without re-describing what a lead is.
- one person's chat → the
Decide what happens. One of these, not several. Every one of them except the first two is a tool call, and an agent may only call what its allow-list names: a new agent's is EMPTY, so put the tool in
allowed_toolswithagents.update(or at creation) before expecting the reaction to happen. Without it the model has no such tool and writes something that looks like the call into its reply instead.- answer the customer — the ordinary case, an agent replies;
- a fixed reply with no model at all — put the text in the trigger's template condition and a rule_based agent sends it verbatim, free and instant, substituting {from_name} and {message_text}. The cheapest right answer to "reply to every new lead with our hours". Add a handoff condition and a thinking agent runs behind the canned reply;
- tell the owner — the agent calls
messages.send, and on several channels nothing has to exist first:recipient_usernameresolves a Telegram handle and creates the contact and the thread, a bare number works on WhatsApp and Zalo, and email goes throughmessages.send_emailwithrecipient_email. For someone already in contactsrecipient_nameis enough. Anywhere else it is a thread id, so have them write once from their personal account to a connected number and find that thread withsearch.threads. Setfrom_account_idwhen the workspace has several numbers and one of them must not be the sender; - hand it to a person —
job.escalateflags the thread in the inbox and stops the agent talking over them; - put it on the board —
tasks.createleaves the thread and the facts with someone, so nothing is lost when nobody is looking; - wake a Claude Code session — the advanced option below.
Create the trigger with
agents.trigger_create. The narrowing from step 1 goes inconditions; scoping to particular chats goes in thethread_idsparameter beside it.agents.trigger_create( agent_id=<the agent that reacts>, trigger_type="incoming_message", thread_ids=[<the chat to watch>], conditions={ "keywords": ["invoice", "payment"], "context_types": ["dm"], "cooldown_seconds": 60, }, send_mode="draft", )send_modedraft means a person approves the reply before it goes out, and it is the right setting for step 2's first two options while the rule is new. It is the WRONG setting for a rule whose reaction is itself a message to somebody else: on draft, sending is intercepted, so the note to the owner becomes another draft waiting in the inbox and the rule reports success having told nobody. That rule wants auto from the start. Filing a task and escalating are not sends, and happen either way.- Stop it becoming noise. cooldown_seconds sets a minimum gap per conversation and max_runs_per_thread_per_hour a ceiling. once_per_thread makes the rule act once in a conversation and then step aside — right for a rule that files, tags or hands off, wrong for the agent that is holding the conversation.
Prove it fires, and prove it stays quiet.
agents.simulate_inboundreplays a message through the real router and answers which trigger won (with its conditions and why it was picked) or, on a miss, the reason it matched nothing. It drafts by default, so nobody is messaged: give it thethread_idthe rule watches and onemessage_textthat must match, then one that must not. A rule that never fires and a rule that fires on everything look identical from the outside until this is run.Two things to know about that run. It is a REAL run of the matched agent, so its non-sending work happens for real: simulating a rule that files a task files one, and simulating an escalation pauses the thread. And the simulator deliberately skips the trigger's rate limits so repeated tests do not trip them, which makes step 4's two caps the one part of the rule a simulation cannot check — send two matching messages for real, or trust them.
Waking a Claude Code session (advanced)
A session can be the thing that reacts: the event reaches it, it reads the threads, and it answers. Right when the reaction needs judgement across several conversations rather than one reply.
There are two routes, and the cheap one is easy to miss:
Point a trigger straight at the session. Make the agent with
agents.create, text engine claude_channels (the "Claude Code" card in the New AI Agent dialog), and give it the trigger from step 3. It runs no model of its own, so this costs NOTHING inside the workspace: no intermediate agent, no tokens, the thinking happens in the session. It answers the conversation withmessages.sendand closes the run withagents.task_complete. Name the machine withtarget_sessiononagents.trigger_create, the way the desktop list spells it — without it the run is deliverable only while exactly ONE desktop is connected and refuses the moment a second window opens.Step 3's draft advice does not apply here. Nothing gates this route on the trigger's send mode, and the session replies over its own connection, which always delivers — so there is no draft to approve and the first live message reaches the customer. Try it on a chat of your own before pointing it at one.
- Hand it a task — the reacting agent ends its run with
tasks.createnaming the machine, and the session reports back withtasks.comment. Use it when several desktops are connected, or when the work wants a status and a history.
The allow-list from step 2 applies to the DialogBrain agent that hands the
work over, not to the session: that agent needs tasks.create, while the
session reaches the workspace over MCP with its own key and is not limited by
any agent's list.
Check first. Call workspace.desktops. If this machine is not in that
list, nothing in this section can reach you — use step 2's options instead.
Two things have to be true, and both are one-time setup:
- the channel needs an API key and a name for this machine, in
~/.claude/channels/dialogbrain/.env; - the session must have been STARTED with pushes enabled. On a person's own
computer that is
claude --dangerously-load-development-channels plugin:dialogbrain@dialogbrain(add--resume <session id>to restart the same session; it keeps its id). The shortclaude --channels plugin:dialogbrain@dialogbrainworks only where managed settings allowlist the plugin (channelsEnabled,allowedChannelPlugins) and is refused elsewhere ("not on the approved channels allowlist"). Without either, Claude Code silently drops every push, and the only trace is a line in the session's own MCP log. A channel configured by hand instead of coming from the plugin is namedserver:<name>; keep only one DialogBrain channel per session. - only messages from the other side wake it: what the connected account sends itself is outgoing. Do not test a trigger by writing from that account. Every entry carries a tag — a bare name is refused before the session starts.
The flag is read at start, so switching it on means restarting the session.
The channel ships with this plugin and reads
~/.claude/channels/dialogbrain/.env once at start (an API key from the
cabinet under Settings → Developer and a stable name for this machine; the
server address comes from the plugin), so restart Claude Code after editing
it. That name is the address, and it matters because the gateway
hands an event to every connection the account has: with several sessions
connected and none named, the work is deliberately delivered to none rather
than to all of them.
| Error | What it means |
channel_not_connected | No desktop registered. Usually the channel is not running, or points at a local server. |
channel_target_missing | Several desktops, the task names none. |
channel_target_offline | The name given is not among the live sessions. |
channel_target_ambiguous | Two sessions share that name; target one by its session id. |
The mistake to avoid
Reacting to everything. At a hundred conversations a day a rule on every message produces a hundred identical notifications, and the one that mattered is lost among them. Narrow first, then decide what happens.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

