Via the feishu-cli event subcommand family, subscribe to Feishu Open Platform events, receive events over a WebSocket long connection, and output them to stdout as NDJSON. Suitable for AI Agent scenarios such as real-time bot responses, group message monitoring, and approval callback consumption.
feishu-cli: if not yet installed, go to riba2534/feishu-cli to get the installation method.
Send a message? Please use the feishu-cli-msg skill. This skill focuses on event subscription (receivingapp events) and is not responsible for sending.
Core concepts
Process model = 1 EventKey, 1 consume process
event consume <EventKey>
│
├─ 启动 WebSocket 长连接(飞书 SDK ws.Client + AutoReconnect)
├─ 注册到 bus.json(PID / EventKey / 启动时间 / max-events / timeout)
├─ stderr 输出 [event] ready event_key=<key>
├─ 接收事件 → 写 stdout(NDJSON,每条一行 JSON)
├─ 可选:dump 每条事件为 <event_id>.json 文件
├─ 退出条件:--max-events / --timeout / SIGTERM / Ctrl-C / stdin EOF / pipe broken
└─ 退出时自动 unregister bus.json
Differences from lark-cli event: lark-cli uses a Unix domain socket to run an independent bus daemon for event fan-out; feishu-cli simplifies this to "each consume directly connects one WebSocket" and does no event distribution—enough to cover the mainline AI Agent scenario of single-EventKey subscription.
State files and cross-process mutual exclusion
| Path | Purpose |
|---|---|
~/.feishu-cli/events/<app_id>/bus.json |
List of active consumers (PID/EventKey/start time/parameters) |
~/.feishu-cli/events/<app_id>/bus.lock |
flock file lock; bus.json reads and writes are serialized, and the fd is automatically released when closed |
One subdirectory per AppID, so different apps do not interfere with each other.event status The query actively removes entries for PIDs that are no longer alive (kill -9 / crash leftovers). bus.json uses tmp + rename atomic writes to prevent partial writes.
Output protocol (NDJSON + ready marker)
| Stream | Content |
|---|---|
| stdout | One line of JSON per event (NDJSON), suitable for jq / script pipelines |
| stderr | Diagnostic logs; at startup, one line [event] ready event_key=<key> (init complete; WS handshake in progress) |
AI Agent recommendation: the parent process runs consume in the background (
run_in_background=true), first blocks on stderr until the[event] readyline, then starts reading stdout.Note: the ready marker only indicates that process initialization is complete; the WS handshake is performed asynchronously in the background; after the parent process sees the marker,it still needs to wait an additional 1-3s for the WS handshake to actually complete. In production, it is recommended that the parent process send a self-check event + wait for the echo round trip to confirm the link is working.
Exit codes and exit reason
| Exit code | Meaning |
|---|---|
| 0 | Normal exit (reached --max-events / --timeout / SIGTERM / Ctrl-C / stdin EOF) |
| Non-0 | startup failure / unrecoverable WebSocket error / parameter error |
At the end of stderr, [event] exited — elapsed=<d> reason=<r>is output; there are 4 reasons:
limit— reached--max-eventstimeout— reached--timeoutsignal— context canceled (Ctrl-C / SIGTERM / stdin EOF / downstream pipe broken)error— WebSocket connection keeps failing
Command quick reference
feishu-cli event list [--json] # 1. 列所有支持的 EventKey
feishu-cli event schema <event_key> [--json] # 2. 看某 key 的 EventType / scope / payload schema
feishu-cli event consume <event_key> [flags] # 3. 启动订阅(阻塞)
feishu-cli event status [--json] # 4. 看本机活跃 consume 进程
feishu-cli event stop {--pid N | --event-key K | --all} [--force] [--json] # 5. 停 consume
1. event list: list supported EventKeys
Display the currently supported 22+ EventKeys grouped by domain (im / contact / calendar / drive / approval / vc).
# 表格视图(默认)
feishu-cli event list
# JSON 输出,jq 提取 IM 域所有 EventKey
feishu-cli event list --json | jq -r '.[] | select(.domain=="im") | .key'
Output fields(JSON mode):key / event_type / description / domain / scopes[] / payload_schema。
2. event schema: view payload schema and scope
feishu-cli event schema im.message.receive_v1
feishu-cli event schema im.message.receive_v1 --json
Output 4 parts:Key / Event Type / Domain / Description / Scopes + optional Payload Schema (示例)。The payload schema is manually curated; after subscribing, the actual payload is subject to the Feishu Open Platform documentation.
3. event consume:Start WebSocket subscription (blocking)
# 基础订阅,Ctrl-C 退出
feishu-cli event consume im.message.receive_v1
# 调试:抓 5 条消息,最多跑 60s
feishu-cli event consume im.message.receive_v1 --max-events 5 --timeout 60s
# 落盘 + 静默
feishu-cli event consume im.message.receive_v1 --output-dir ./events --quiet
# 配合 jq 实时过滤群消息
feishu-cli event consume im.message.receive_v1 | jq 'select(.event.message.chat_type=="group")'
# 后台并发订阅多个 EventKey(每个 EventKey 一个进程)
feishu-cli event consume im.message.receive_v1 > receive.ndjson 2> receive.log &
feishu-cli event consume im.message.reaction.created_v1 > reaction.ndjson 2> reaction.log &
feishu-cli event status
key flag:
| Flag | Default | Description |
|---|---|---|
--max-events N |
0 (unlimited) | Exit after receiving N events, reason=limit |
--timeout <duration>(example 60s) |
0 (unlimited) | Exit after running for duration D, reason=timeout |
--jq .event.xxx |
"" | Minimaldot pathfiltering; full jq syntax is not supported (use pipe to external jq) |
--output-dir ./events |
"" | Additionally dump each event as <event_id>.json to disk (does not affect stdout) |
--quiet |
false | Suppress stderr diagnostics;AI Agents use with caution—it will suppress most stderr together, but the ready marker still goes through the real os.Stderr and is unaffected |
--jq limitation:only recognizes .a.b.c map value access of the form (e.g. .event.message), does not support select / array indexing / pipes. For complex filtering, use feishu-cli event consume ... | jq '<expr>'。
--output-dir limitation:must be a safe relative path; no ~ expansion, does not accept absolute paths or .. path segments.
4. event status:View active consume processes on this machine
feishu-cli event status
feishu-cli event status --json | jq '.consumers[] | .pid'
Output:App ID / State file path / PID / EVENT_KEY / UPTIME / EXTRA(max-events / timeout / output-dir / jq)。
When querying, entries for PIDs that are no longer alive are proactively removed (cleaning up zombie records left by kill -9 / crashes).
5. event stop:Stop consume processes
feishu-cli event stop --pid 12345 # 按 PID
feishu-cli event stop --event-key im.message.receive_v1 # 按 EventKey(所有订阅该 key 的进程)
feishu-cli event stop --all # 当前 AppID 下全部 consume
feishu-cli event stop --all --force # SIGKILL(紧急情况)
By default SIGTERM for graceful exit (the consume process will automatically unregister bus.json), wait up to 3s to verify the process has exited;--force escalates to SIGKILL, which will leave zombie entries in bus.json; next time event status will automatically clean them up.
EventKey Quick Reference (grouped by domain)
For the full list use feishu-cli event list. Common ones:
| Domain | EventKey | Description |
|---|---|---|
| im | im.message.receive_v1 |
Receive messages (users/group chats send to the Bot) |
| im | im.message.message_read_v1 |
Message read receipt |
| im | im.message.recalled_v1 |
Message recalled |
| im | im.message.reaction.created_v1 / deleted_v1 |
Message emoji reaction added/removed |
| im | im.chat.updated_v1 |
Group chat information updated |
| im | im.chat.member.user.added_v1 / deleted_v1 |
User joins/leaves group |
| im | im.chat.member.bot.added_v1 / deleted_v1 |
Bot is added to/removed from group |
| im | im.chat.disbanded_v1 |
Group chat disbanded |
| contact | contact.user.created_v3 / updated_v3 / deleted_v3 |
Employee onboarding/change/offboarding |
| calendar | calendar.calendar.event.changed_v4 |
Calendar event changes (create/update/delete) |
| calendar | calendar.calendar.acl.created_v4 |
Calendar permission changes |
| drive | drive.file.title_updated_v1 |
Document title modified |
| drive | drive.file.permission_member_added_v1 |
Document collaborator added |
| approval | approval_instance |
Approval instance status changed |
| approval | approval_task |
Approval task changed |
| vc | vc.meeting.meeting_started_v1 / meeting_ended_v1 |
VC meeting started/ended |
EventKey and EventType are usually the same;in the received payload,
header.event_typeequals theevent_typefield.
Permissions and Open Platform configuration
Default App Token, no need for auth login
Event subscription uses App identity (app_id + app_secret),does not require a user token. Configure ~/.feishu-cli/config.yaml or FEISHU_APP_ID / FEISHU_APP_SECRET environment variables and you're set.
Feishu Open Platform two-step configuration
In open.feishu.cn your app console:
- "Event Subscription - Receive Events via Long Connection" enable long connection mode (feishu-cli uses WebSocket,not webhook URL mode)
- "Events & Callbacks - Event Subscriptions" Select the target EventType (consistent with the
event schema <key>output Event Type) andpublish version - scope enablement: the scope required by each EventKey is shown in
event schema <key>'sScopesfield; enable it on the "Permission Management" page.eventdomain has been added to the--domain event --recommendrecommended list, allowing a one-time application for the union of common scopes IM/contact/calendar/drive/approval/vc
Common Errors
| Symptom | Cause | Solution |
|---|---|---|
| WS connection failed, stderr reports ws error | Long connection mode not enabled | Enable "Event Subscriptions - Receive events via long connection" in the Feishu Open Platform |
| After startup it shows ready, but no events are received | The target EventType is not checked under "Event Subscriptions" / version not published | Re-check + publish version |
| Events received but payload fields are missing | The App is missing the corresponding scope (e.g. im:message.p2p_msg:readonly) |
event schema <key> Check Scopes, go to the Permission Management page to enable it, then re-subscribe |
event consume exits immediately with reason=error |
Wrong App ID/Secret / network unreachable / the domain uses lark but BaseURL uses feishu | Check config.yaml;lark International version requires --base-url https://open.larksuite.com or the corresponding configuration |
Recommended usage for AI Agent backend subscription
Single EventKey backend subscription (run_in_background=true)
# 1. 后台启动 consume,stderr/stdout 各 redirect
task = Bash(
command='feishu-cli event consume im.message.receive_v1 --output-dir ./events 2> consume.log',
run_in_background=True,
)
# 2. tail consume.log 阻塞等 "[event] ready event_key=im.message.receive_v1"
# 3. 额外 sleep 1-3s 让 WS 握手完成
# 4. 业务逻辑:tail stdout / 读 ./events/*.json 处理新事件
# 5. 退出:feishu-cli event stop --event-key im.message.receive_v1
# 或父进程 kill 后台 Bash task(SIGTERM 触发 graceful shutdown + unregister)
subprocess stdin EOF protocol (non-TTY)
In non-TTY mode,closing stdin triggers graceful exit(reason=signal)。Python subprocess.Popen use stdin=subprocess.PIPE, after processing p.stdin.close() is more stable than SIGTERM—consume will finish the current event before exiting.
Limit single-run duration / event count
For debugging scenarios always first use --max-events N --timeout Ds, to avoid forgetting to stop and leaving a background process consuming API quota:
feishu-cli event consume im.message.receive_v1 --max-events 1 --timeout 30s
# 抓 1 条事件 demo / 30 秒超时双保险
Pitfalls and Notes
- daemon process persistence:
event consumeblocks running until a signal/timeout/EOF;will not exit on its own. Running in the background as an AI Agent must be configured with--max-events/--timeoutor explicitevent stop, otherwise it will leave a long-running process - flock cross-process mutual exclusion: bus.json reads and writes both use flock, multiple
event consumestarting and registering at the same time is safe; butdo not edit manually bus.json - pipe broken auto-exit: downstream jq / tee closing stdout (typical scenario:
event consume ... | head -1) triggers SIGPIPE, consume proactively cancels and exits with reason=signal, and will not hang waiting for Ctrl-C --quietdoes not affect the ready marker: the ready marker goes through the realos.Stderrbypassing--quietredirection, so even if an AI Agent enables--quietthe parent process can still wait for the ready line; but other diagnostics (including[event] exitedreason) will be silently- AutoReconnect infinite retry: oapi-sdk-go v3 ws.Client defaults to
WithAutoReconnect(true), retrying infinitely after disconnection (interval 2 minutes + initial jitter). For long disconnection scenarios it is recommended to use--timeoutto actively exit and have the outer daemon process pull it up, which is more controllable than infinite inner retry - status does not actively ping the process:
event statususedsignal(0)for liveness checks; in PID reuse scenarios it could theoretically misjudge (extremely low probability).event stop --pid Nis also syscall.Kill; hitting the wrong PID will fail with ESRCH, and will not kill the wrong process - Each event gets its own file:
--output-dirmode, each event is written to disk<event_id>.json,Short-term high-frequency events may create a large number of small files; writing to disk is only for record-keeping, and stdout NDJSON is still recommended for business consumption --jqonly supports dot paths:--jq .event.messageprojects each event onto the subtree before output; events that do not matchwill be skipped(no blank lines are output). For complex filtering, always use external pipe jq--output-dironly supports safe relative paths: passing~/events、/tmp/events、../eventswill all error; use./eventsorevents/today
When to switch to other skills
| Task | Routing |
|---|---|
| SendMessages / replies / cards / notifications | feishu-cli-msg |
| Construct interactive card JSON | feishu-cli-card |
| Handle received message events → write to multi-dimensional table | feishu-cli-bitable(after parsing the payload, call the record command) |
| Handle received approval events → query approval details | feishu-cli-toolkit(approval subcommand) |
| After receiving group messages, query group info/members | feishu-cli-chat |
| Webhook URL mode (HTTP callback, not a long connection) | Not within the scope of this skill; use the Feishu Open Platform's "Request URL configuration" + a self-built HTTP server |
| Batch pull of historical messages (not real-time) | feishu-cli-chat the msg history / msg list |
Reference
- Feishu Open Platform event subscription documentation:https://open.feishu.cn/document/server-docs/event-subscription-guide/event-list
- Project CHANGELOG: for details on new additions to this module, see the repository
CHANGELOG.mdevent 模块section - Source code:
cmd/event*.go+internal/event/{bus,keys,runtime}.go
Security — event_id filename sanitization
--output-dir When enabled, each event is dumped as <event_id>.json. The v1 PR adds sanitizeEventID defenses:
- Only
[A-Za-z0-9_-]characters are kept, truncated to 128 in length ..、/, spaces, and special symbols are all discarded- If the sanitized result is an empty string → skip the dump (do not write a file with an empty filename)
Defense scenario: when the server payload is abnormal or maliciously crafted header.event_id = "../etc/passwd" -type payloads, writeFile will not escape --output-dir。