Feishu Real-time Event Subscription Skill (WebSocket)
Subscribe to Feishu Open Platform events via the
subcommand family. Use WebSocket long connections to receive events and output them to stdout in NDJSON format, suitable for scenarios like AI Agent bot real-time response, group message monitoring, approval callback consumption, etc.
feishu-cli: If not installed yet, please visit
riba2534/feishu-cli for installation instructions.
Need to send messages? Please use the feishu-cli-msg skill. This skill focuses on event subscription (receiving app events) and does not handle sending.
Core Concepts
Process Model = One consume process per EventKey
event consume <EventKey>
│
├─ Starts WebSocket long connection (Feishu SDK ws.Client + AutoReconnect)
├─ Registers to bus.json (PID / EventKey / start time / max-events / timeout)
├─ Outputs [event] ready event_key=<key> to stderr
├─ Receives events → writes to stdout (NDJSON, one JSON per line)
├─ Optional: dumps each event as <event_id>.json file
├─ Exit conditions: --max-events / --timeout / SIGTERM / Ctrl-C / stdin EOF / pipe broken
└─ Automatically unregisters from bus.json on exit
Differences from lark-cli event: lark-cli runs an independent bus daemon via Unix domain socket for event fan-out; feishu-cli simplifies it to "each consume connects directly to a WebSocket" without event distribution—this sufficiently covers the main scenario of AI Agent single EventKey subscription.
Status 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; serializes bus.json read/write, automatically released when fd is closed |
One subdirectory per AppID, different applications do not interfere with each other.
will actively remove entries of non-living PIDs (residues from kill -9 / crashes). bus.json uses tmp + rename for atomic writing to prevent partial writes.
Output Protocol (NDJSON + ready marker)
| Stream | Content |
|---|
| stdout | One JSON per event line (NDJSON), suitable for jq / script pipelines |
| stderr | Diagnostic logs; outputs [event] ready event_key=<key> (init complete; WS handshake in progress)
on startup |
AI Agent Recommendation: Run consume in the background with the parent process (
), block on stderr until the
line appears before starting to read stdout.
Note: The ready marker only indicates process initialization is complete, the WS handshake is executed asynchronously in the background; after seeing the marker, the parent process
needs an additional 1-3s wait for the WS handshake to truly complete. In production environments, it is recommended that the parent process sends a self-check event + waits for echo loop to confirm the link is connected.
Exit Codes and Exit Reasons
| Exit Code | Meaning |
|---|
| 0 | Normal exit (reached / / SIGTERM / Ctrl-C / stdin EOF) |
| Non-0 | Startup failure / unrecoverable WebSocket error / parameter error |
The end of stderr will output
[event] exited — elapsed=<d> reason=<r>
, where there are 4 reasons:
- — reached
- — reached
- — context canceled (Ctrl-C / SIGTERM / stdin EOF / downstream pipe broken)
- — continuous WebSocket connection failure
Quick Command Reference
bash
feishu-cli event list [--json] # 1. List all supported EventKeys
feishu-cli event schema <event_key> [--json] # 2. View EventType / scope / payload schema of a key
feishu-cli event consume <event_key> [flags] # 3. Start subscription (blocking)
feishu-cli event status [--json] # 4. View active local consume processes
feishu-cli event stop {--pid N | --event-key K | --all} [--force] [--json] # 5. Stop consume
1. : List supported EventKeys
Displays over 22 currently supported EventKeys grouped by domain (im / contact / calendar / drive / approval / vc).
bash
# Table view (default)
feishu-cli event list
# JSON output, extract all IM domain EventKeys with jq
feishu-cli event list --json | jq -r '.[] | select(.domain=="im") | .key'
Output fields (JSON mode):
/
/
/
/
/
.
2. : View payload schema and scope
bash
feishu-cli event schema im.message.receive_v1
feishu-cli event schema im.message.receive_v1 --json
Outputs 4 parts:
/
/
/
/
+ optional
. The payload schema is manually curated; the actual payload after subscription is subject to Feishu Open Platform documentation.
3. : Start WebSocket subscription (blocking)
bash
# Basic subscription, exit with Ctrl-C
feishu-cli event consume im.message.receive_v1
# Debug: capture 5 messages, run for maximum 60s
feishu-cli event consume im.message.receive_v1 --max-events 5 --timeout 60s
# Persist to disk + quiet mode
feishu-cli event consume im.message.receive_v1 --output-dir ./events --quiet
# Real-time filter group messages with jq
feishu-cli event consume im.message.receive_v1 | jq 'select(.event.message.chat_type=="group")'
# Background concurrent subscription to multiple EventKeys (one process per 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 flags:
| Flag | Default | Description |
|---|
| 0 (unlimited) | Exit after receiving N events, reason= |
| 0 (unlimited) | Exit after running for D duration, reason= |
| "" | Minimal dot-path filtering, does not support full jq syntax (use pipe with external jq) |
| "" | Additionally dump each event as to disk (does not affect stdout) |
| false | Suppress stderr diagnostics; use with caution for AI Agent—most stderr will be suppressed, but the ready marker still uses real os.Stderr and is not affected |
Limitations: Only recognizes map values in the form of
(e.g.,
), does not support
/ array subscripts / pipes. For complex filtering, use
feishu-cli event consume ... | jq '<expr>'
.
Limitations: Must be a safe relative path; does not expand
, does not accept absolute paths or
path segments.
4. : View active local consume processes
bash
feishu-cli event status
feishu-cli event status --json | jq '.consumers[] | .pid'
Outputs:
/
path /
/
/
/
(max-events / timeout / output-dir / jq).
During query, entries of non-living PIDs will be actively removed (cleaning up zombie records from kill -9 / crashes).
5. : Terminate consume processes
bash
feishu-cli event stop --pid 12345 # By PID
feishu-cli event stop --event-key im.message.receive_v1 # By EventKey (all processes subscribing to this key)
feishu-cli event stop --all # All consume processes under current AppID
feishu-cli event stop --all --force # SIGKILL (emergency situation)
Default uses SIGTERM for graceful exit (consume process will automatically unregister from bus.json), waits up to 3s to verify the process has exited;
upgrades to SIGKILL, which will leave zombie entries in bus.json, and the next
will automatically clean them up.
EventKey Quick Reference (Grouped by Domain)
Use
for the complete list. Common ones:
| Domain | EventKey | Description |
|---|
| im | | Receive messages (sent to Bot by user/group chat) |
| im | im.message.message_read_v1
| Message read receipt |
| im | | Message recalled |
| im | im.message.reaction.created_v1
/ | Message reaction added/deleted |
| im | | Group chat information updated |
| im | im.chat.member.user.added_v1
/ | User joined/left group |
| im | im.chat.member.bot.added_v1
/ | Bot added/removed from group |
| im | | Group chat disbanded |
| contact | / / | Employee hired/changed/left |
| calendar | calendar.calendar.event.changed_v4
| Calendar event changed (created/updated/deleted) |
| calendar | calendar.calendar.acl.created_v4
| Calendar permission changed |
| drive | drive.file.title_updated_v1
| Document title modified |
| drive | drive.file.permission_member_added_v1
| Document collaborator added |
| approval | | Approval instance status changed |
| approval | | Approval task changed |
| vc | vc.meeting.meeting_started_v1
/ | VC meeting started/ended |
EventKey and EventType are usually consistent; the
in the received payload equals the
field.
Permissions and Open Platform Configuration
Default App Token, no required
Event subscription uses App identity (app_id + app_secret),
user token is not mandatory. Just configure
~/.feishu-cli/config.yaml
or set the
/
environment variables.
Two-step Configuration on Feishu Open Platform
In your application console at
open.feishu.cn:
- 「Event Subscription - Receive Events via Long Connection」 Enable long connection mode (feishu-cli uses WebSocket, not webhook URL mode)
- 「Events and Callbacks - Event Subscription」 Select the target EventType (consistent with the Event Type output by ) and publish the version
- Scope activation: The scopes required for each EventKey can be found in the field of ; activate them on the "Permission Management" page. The domain has been added to the
--domain event --recommend
recommendation list, allowing one-click application for the union of common scopes for IM/contact/calendar/drive/approval/vc
Common Errors
| Phenomenon | Cause | Solution |
|---|
| WS connection failed, stderr reports ws error | Long connection mode not enabled | Enable "Event Subscription - Receive Events via Long Connection" on Feishu Open Platform |
| Ready message appears after startup, but no events are received | Target EventType not checked in "Event Subscription" / version not published | Re-check and publish the version |
| Events are received but payload fields are missing | App lacks corresponding scopes (e.g., im:message.p2p_msg:readonly
) | Check Scopes via , activate on the permission management page and re-subscribe |
| exits immediately with reason=error | Incorrect App ID/Secret / network unreachable / using lark domain but BaseURL set to feishu | Check ; for lark international version, use --base-url https://open.larksuite.com
or corresponding configuration |
Recommended Usage for AI Agent Background Subscription
Single EventKey Background Subscription ()
python
# 1. Start consume in background, redirect stderr/stdout respectively
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 and block until "[event] ready event_key=im.message.receive_v1"
# 3. Additional sleep 1-3s to complete WS handshake
# 4. Business logic: tail stdout / read ./events/*.json to process new events
# 5. Exit: feishu-cli event stop --event-key im.message.receive_v1
# Or kill the background Bash task from parent process (SIGTERM triggers graceful shutdown + unregister)
Subprocess stdin EOF Protocol (Non-TTY)
In non-TTY mode,
closing stdin triggers graceful exit (reason=signal). For Python
with
,
after processing is more stable than SIGTERM—the consume process will finish processing the current event before exiting.
Limit Single Run Duration / Event Count
Always use
--max-events N --timeout Ds
in debugging scenarios to avoid leaving background processes that consume API quota:
bash
feishu-cli event consume im.message.receive_v1 --max-events 1 --timeout 30s
# Capture 1 event demo / 30-second timeout double insurance
Pitfalls and Notes
- Persistent daemon process: runs blocking until signal/timeout/EOF; it will not exit on its own. When running in the background for AI Agent, you must configure / or explicitly run , otherwise long-running processes will remain.
- flock cross-process mutual exclusion: bus.json read/write operations all use flock, so registering multiple processes simultaneously is safe; do not edit bus.json manually.
- Automatic exit on pipe broken: If downstream jq / tee closes stdout (typical scenario:
event consume ... | head -1
), SIGPIPE will be triggered, and consume will actively cancel exit with reason=signal, avoiding being stuck waiting for Ctrl-C.
- does not affect ready marker: The ready marker uses real bypassing redirection, so even if is enabled, the parent process can still wait for the ready line; but other diagnostics (including reason) will be suppressed.
- AutoReconnect infinite retry: oapi-sdk-go v3 ws.Client defaults to , retries infinitely after disconnection (interval 2 minutes + initial jitter). For long-term disconnection scenarios, it is recommended to use to exit actively and be pulled up by the outer daemon process, which is more controllable than infinite inner retry.
- status does not actively ping processes: uses to check liveness, which may theoretically misjudge in PID reuse scenarios (very low probability). also uses syscall.Kill, which will fail with ESRCH if the wrong PID is hit, avoiding accidental killing.
- Independent file per event: In mode, each event is persisted as , a large number of small files may be created for high-frequency events in a short time; persistence is only for traceability, it is recommended to use stdout NDJSON for business consumption.
- only supports dot paths: projects each event to the subtree before output; events that do not match will be skipped (no empty lines output). Always use pipe with external jq for complex filtering.
- only supports safe relative paths: Passing , , will all report errors; use or .
When to Switch to Other Skills
| Task | Route |
|---|
| Send messages / replies / cards / notifications | feishu-cli-msg |
| Construct interactive card JSON | feishu-cli-card |
| Process received message events → write to multidimensional tables | feishu-cli-bitable (parse payload and call record command) |
| Process received approval events → query approval details | feishu-cli-toolkit (approval subcommand) |
| Query group information/members after receiving group messages | feishu-cli-chat |
| Webhook URL mode (HTTP callback, non-long connection) | Not within the scope of this skill; use "Request URL Configuration" on Feishu Open Platform + self-built HTTP server |
| Batch pull historical messages (non-real-time) | / from feishu-cli-chat |
References
- Feishu Open Platform Event Subscription Documentation: https://open.feishu.cn/document/server-docs/event-subscription-guide/event-list
- Project CHANGELOG: Details of new additions to this module can be found in the section of the repository's
- Source Code: +
internal/event/{bus,keys,runtime}.go
Security — event_id Filename Sanitization
When
is enabled, each event is dumped as
. The v1 PR added
for defense:
- Only retains characters, truncates to 128 characters in length
- , , spaces, and special symbols are discarded
- If sanitization results in an empty string → skip dumping (do not write a file with empty filename)
Defense scenario: When the server payload is abnormal or maliciously constructed (e.g.,
header.event_id = "../etc/passwd"
), writeFile will not escape from
.