Scenario
A new member joins and the bot says nothing. A friend request arrives and it sits there. The bot is pulled into another group and does nothing there either — and that is usually not the adapter's fault: only messages can travel down the current path.
In 4.10 a bot had a single entry point: use_pipeline_name, use_pipeline_uuid and pipeline_routing_rules (measured 2026-10-16 on a live v4.10.11 instance), and its rules could only filter launcher_type, launcher_id, message_content and message_has_element (source src/langbot/pkg/persistence/alembic/versions/0009_migrate_event_bindings.py:35-52). A QQ group bot should answer questions by day, welcome new members and hand friend requests to a human; on 4.10 you could only split messages across pipelines.
How it works in 4.11
4.11 replaces "bot → one pipeline" with "bot → event bindings". Each binding answers three questions: which event, under which conditions, to whom.
Data model (new in 4.11). Bindings live in new JSON columns on the bots table: event_bindings and plugin_processors (source src/langbot/pkg/entity/persistence/bot.py:55-56). There is no separate routing table; the three legacy columns are dropped by 0011_drop_legacy_bot_routing.py. One binding carries (official docs docs/event-based-agents/04-event-routing.md:69-73, plus order at runtime):
-
event_pattern— the event type: exact match,namespace.*, or the global*(sourcesrc/langbot/pkg/platform/botmgr.py:152-159). -
filters— conditions onchat_type,chat_id,message_text,message_element_types; operators includeeq,neq,contains,not_contains,starts_with,regex; all of them must hold. -
target_type+target_uuid—pipeline,agentordiscard(in the UI: Pipeline / Agent / Discard). -
priority— higher wins;enabled— false eliminates the row. -
order— list position, written from the array index on save (sourcesrc/langbot/pkg/api/http/service/bot.py:257); dragging a row edits it.
Arbitration. An event ends up with exactly one response target. Disabled bindings are dropped, the event type is matched, the target is checked, filters are evaluated, and survivors are sorted by (priority, -order) descending — first wins (source botmgr.py:274-345, collect :330, sort :335; official docs 04-event-routing.md:115-119). Losers are recorded as lower_priority, so dragging a row up and raising its priority mean the same thing.
Event naming (new in 4.11). Event types are {namespace}.{action} strings (official docs docs/event-based-agents/01-event-system.md:5), e.g. message.received, group.member_joined, friend.request_received, feedback.received. Platform events use platform.{adapter}.{action} (:447-451). Support depends on the adapter and platform — check the support matrix first.
Separate 4.10 from what is new. Adapters are unchanged: http_bot.py is byte-identical between v4.10.11 and v4.11.0 (27,029 bytes), and 4.10 already shipped aiocqhttp, kook and wecombot. What is new is the event-binding data model and routing mechanism — adapters emit events, bindings pick the handler. A legacy adapter declaring no supported_events only emits message.received (source EventBindingsEditor.tsx). Support depends on the adapter and platform — check the support matrix first.
Upgrading is not a blank slate. 0009_migrate_event_bindings.py turns use_pipeline_uuid into one binding and each pipeline_routing_rules entry into another (launcher_type→chat_type, launcher_id→chat_id, message_content→message_text, message_has_element→message_element_types, eq→contains, neq→not_contains; source :38-52). Bindings keep the same pipeline UUID; no agent or Runner configuration is created. That migration has an empty downgrade (source :133, not reversible), and 0035_pipeline_received_only.py later narrows message.* to message.received.
Hands-on steps
Start a fresh 4.11 instance (container name and data directory are yours to change):
docker network create lang7-411
docker run -d --name lang7-411-core --network lang7-411 \
-e BOX__ENABLED=false -e SPACE__DISABLE_TELEMETRY=true -p 15400:5300 \
-v "$PWD/deploy/411/data:/app/data" rockchin/langbot:v4.11.0
The first boot runs the migrations first. Open http://127.0.0.1:15400 and sign in as admin@langbot.local (the password is a local demo value — replace it). Then:
- Create or select a bot (the demo uses the generic HTTP adapter).
- On the bot detail page, find the Event Routing card — it decides which processor gets an incoming event.
- A new bot already has a
message.receivedroute pointing at a pipeline. The supported-events header groups events by what the adapter declares (here: Messages · 1).
- Click Add behavior and pick a preset (reply to messages, welcome new members, ...), or use Add route to fill in event and target yourself.
- On the new route row, pick the event and processor: the picker offers existing pipelines and agents, or Create new processor. The screenshot below shows the row added but not yet saved.
- Expand Trigger conditions for filters: chat type, chat ID, message text, message elements — all must hold.
- Drag rows to reorder (
order), change weights with Priority, switch a route off with Enabled. - After saving, run Check routing (a dry run) before live traffic.
An upgraded instance needs no manual re-wiring: copy the 4.10.11 data directory, point the same docker run at it, and legacy rules land as bindings through the mapping above. The field map is in pipeline-migration-config-map.zh-CN.md.
Verify
Three ways, fastest first:
-
Check routing in the card: pick an event type, run one synthetic dispatch, and it lists every binding's outcome — matched / delivered / discarded — or why it lost:
event_pattern_mismatch,filters_mismatch,lower_priority,processor_incompatible(a pipeline pointed at a non-message event). Dry run and real dispatch share one matcher (sourcebotmgr.py:361,diagnose_eba_event_binding). - Match records: recent matches held in the bot's memory. Cleared on restart — use the operation log for long-lived checks.
-
Look at the database:
sqlite3 deploy/411/data/langbot.db "select name, event_bindings from bots". A migrated binding (measured 2026-10-16):{"event_pattern":"message.received","target_type":"pipeline","target_uuid":"2e59bbf9-…","filters":[],"priority":0,"enabled":true,"description":"Migrated from default pipeline binding","order":0}. While the pipeline behindtarget_uuidexists, the route works.
Limits and caveats
- One response target per event. Bindings arbitrate rather than run in parallel: if two match, only the first runs.
-
A pipeline target only accepts
message.received. Any other event fails asprocessor_incompatible— "Pipeline targets only support message events" (sourcebotmgr.py:162,:315-317). To greet someone on join, target an agent or plugin event processor. -
Wildcards were narrowed. Legacy
message.*bindings were rewritten tomessage.receivedby0035. Hand-writtenmessage.*still matches those events, but a pipeline target fails the target check. -
Adapters and events are not one-to-one. Support depends on the adapter and platform — check the support matrix first. The official table (
docs/event-based-agents/01-event-system.md:453-476) calls itself a preliminary assessment: confirm per platform before relying on an event and treat anything unconfirmed as unverified. -
Plugin event processors are a second, parallel channel. The plugin-processors section at the card's bottom takes no part in arbitration: it receives plugin-declared events independently of the routes above (source
botmgr.py:285skips those rows,:1012readsplugin_processorsseparately). A plugin subscribing to an event is not your bot handling it. -
Migration is a one-way door. The binding conversion has no downgrade and
0011drops the old columns; Runner configuration migration is a separate, user-confirmed API. Back up thedatadirectory first.
Summary and next steps
Upgrading from 4.10? Read the field map first: pipeline-migration-config-map.zh-CN.md. See the v4.11.0 release page for this version's scope and the official 01-event-system.md for event types and fields. Next up: Runner plugins — installing one from the marketplace and selecting it in a pipeline.


Top comments (0)