Lark Events skill
Lark/Feishu real-time event listening / subscribing / consuming: stream events as NDJSON via `lark-cli event consume <EventKey>` (covers IM messages/reactions/chat changes, Approval status changes, Task updates, VC meeting started/joined/ended, Minutes generated, Whiteboard updated, etc.).
by larksuite·MIT license·★ 17,534 Stars on the repo·GitHub ↗
npx degit larksuite/cli/skills/lark-event#main ~/.claude/skills/lark-eventChecked ·commit main
Files of Lark Events
Show the full text160 lines
Lark Events
Prerequisite: Read
../lark-shared/SKILL.mdfirst for authentication,--as user/botswitching,Permission deniedhandling, and safety rules.
Core commands
| Command | Purpose |
|---|---|
lark-cli event list [--json] |
List all subscribable EventKeys |
lark-cli event schema <EventKey> [--json] |
Show an EventKey's params and output schema |
lark-cli event consume <EventKey> [flags] |
Blocking consume; events → stdout NDJSON |
lark-cli event status [--json] [--fail-on-orphan] |
Inspect the local bus daemon status |
lark-cli event stop [--all] [--force] |
Stop the bus daemon |
Common flags
| Flag | Description |
|---|---|
--param key=value / -p |
Business params (repeatable; comma-separated for multi-value). Unknown keys fail with valid names listed inline |
--jq <expr> |
jq expression to filter / transform each event; empty output skips the event |
--max-events N |
Exit after N events. Default 0 = unlimited |
--timeout D |
Exit after duration D (e.g. 30s, 2m). Default 0 = no timeout. Whichever of --max-events / --timeout fires first wins |
--output-dir <dir> |
Write each event as a file (relative paths only; prevents traversal) |
--quiet |
Suppress ready/exit markers and per-event stderr diagnostics, including drop warnings. This can hide event loss. AI should not use this — it removes readiness and integrity signals |
--as user|bot|auto |
Identity for the session (see lark-shared) |
Examples
# Default: stream every event for the key (no filter, no projection)
lark-cli event consume im.message.receive_v1 --as bot
# List every EventKey of one domain (the authoritative, always-current catalog)
lark-cli event list --domain vc --json
# Grab one sample event to inspect payload shape
lark-cli event consume im.message.receive_v1 --max-events 1 --timeout 30s --as bot
# Run for 10 minutes then auto-exit
lark-cli event consume im.message.receive_v1 --timeout 10m --as bot
# Consume multiple EventKeys concurrently (one shape per process, no dispatcher)
lark-cli event consume im.message.receive_v1 --as bot > receive.ndjson &
lark-cli event consume im.message.reaction.created_v1 --as bot > reaction.ndjson &
wait
Call flow
lark-cli event list --json→ pick a legal key.--domain <d>narrows to one domain; the domains areapplication,approval,board,card,im,minutes,task,vc. An unknown domain fails with the valid set listed in the hint.lark-cli event schema <key> --json→ readresolved_output_schema+jq_root_pathto determine field pathslark-cli event consume <key> [--jq '<expr>']→ consume
Subprocess contract
Ready marker
event consume's stderr emits a fixed line [event] ready event_key=<key>. Parent processes should block on stderr until this line appears, then start reading stdout. Do not fall back to sleep.
stdin EOF = graceful exit
event consume treats stdin close as a shutdown signal (wired for AI subprocess callers). Bounded runs are exempt: when --max-events or --timeout is set (> 0), stdin EOF is ignored and the run exits only via its own bound, timeout, or SIGTERM. For unbounded runs, < /dev/null / nohup / systemd's default StandardInput=null will cause an immediate graceful exit (stderr reason: signal). To keep an unbounded run alive:
- Feed stdin a source that never EOFs:
< <(tail -f /dev/null) - Or run bounded:
--max-events N/--timeout D
Exit codes & reason
On exit, the last stderr line is [event] exited — received N event(s) in Xs (reason: ...).
| exit code | reason | Trigger |
|---|---|---|
| 0 | reason: limit |
--max-events reached |
| 0 | reason: timeout |
--timeout reached |
| 0 | reason: signal |
Ctrl+C / SIGTERM / stdin EOF (stdin EOF applies to unbounded runs only) |
| 1 | JSON error envelope on stderr | Lark API business failure during pre-consume setup (for example subscription create/delete) |
| 2 | JSON error envelope on stderr (no exited line) |
Validation failure (unknown EventKey, bad --param / --jq, another bus already connected) |
| 3 | JSON error envelope on stderr | Auth failure (missing token, missing scopes) |
| 4 / 5 | JSON error envelope on stderr | Network / internal failure (bus startup, handshake, file I/O) |
Startup and runtime failures emit a structured JSON envelope on stderr: {"ok":false,"error":{"type","subtype","param","message","hint",...}} (the envelope may also carry top-level identity / _notice siblings). Parse error.type / error.subtype to branch (e.g. missing_scope carries a missing_scopes list), error.param to find the offending flag, and error.hint for the recovery action — do not regex-match message text.
Orchestrators should treat reason: limit/timeout/signal (all exit 0) as "business completion" and non-zero as "failure".
Never kill -9
Avoid kill -9 on consume processes for EventKeys whose PreConsume registers a server-side subscription and unsubscribes on exit (minutes, vc, board keys): kill -9 skips the OAPI unsubscribe and leaks the server-side subscription (symptoms: "subscription already exists" on restart, duplicate event delivery). Keys whose subscription is a durable relation with no cleanup (task, approval keys) do not leak this way, but SIGTERM or closing stdin remains the right shutdown for every key.
One consume, one EventKey (multi-key = multi-shell)
The command takes exactly one positional argument; k1,k2 and wildcards are unsupported. Listening to N keys means N subprocesses — this is intentional:
- One shape per process stdout; no dispatcher logic required in the AI
- Fault isolation (one key failing doesn't affect others)
- Independent
--as/--jq/--max-events/--timeoutper key
All N consumers share a single bus daemon (UDS local IPC), so the overhead is small
Writing jq via schema
event schema <key> --json is the source of truth for writing --jq. Four things to look at:
(1) Where fields start — see jq_root_path
- Value
"."→ fields are at the top level, write.chat_id - Value
".event"→ fields are inside a V2 envelope, write.event.chat_id
(2) Field list and types — see resolved_output_schema.properties.<name>
Each field carries type / description, and some also have format. Snippet (from event schema im.message.receive_v1 --json):
{
"chat_id": {"type":"string", "format":"chat_id", "description":"Chat ID, prefixed with oc_"},
"sender_id": {"type":"string", "format":"open_id", "description":"Sender open_id, prefixed with ou_"},
"create_time": {"type":"string", "format":"timestamp_ms", "description":"Send time as ms-epoch string"}
}
(3) Field semantics — see the format tag
Lark-defined semantic tags (not JSON Schema's standard format). Common values: open_id / chat_id / message_id / timestamp_ms / email. Purpose: distinguish "same string type, different meanings" fields so you can reverse-lookup via API or convert formats.
(4) Decoded state — read the field's description
event consume runs Process hooks that may pre-decode some payload fields (flattening V2 envelopes, rendering .content to plain text, etc.) — behavior differs from raw OAPI. Always read the field's description before writing jq, especially for generic field names like content / data / body / payload.
Why it matters: blindly applying fromjson to an already-decoded text field makes jq error on every event and silently drop it — the consumer looks alive but emits nothing, with only a single WARN line buried on stderr. (This is the general behavior: any jq runtime error skips the event with a one-line WARN; the loop does not abort.)
Don't shortcut the schema: when projecting event schema --json with jq, do not strip .description from properties — that's the field that tells you whether a field is already decoded. Dump the full property objects, not just keys.
Aside: --param's valid parameters also live in the schema — the params section lists name / type / required / enum / default / description; section missing = this key accepts no --param.
Topic index
| Topic | Reference | Coverage |
|---|---|---|
| Application | references/lark-event-application.md |
Catalog of Application EventKeys, including application.bot.menu_v6 for custom bot menu push events + flattened event_key / operator fields + jq recipe |
| Approval | references/lark-event-approval.md |
Catalog of 2 Approval EventKeys (approval.instance.status_changed_v4, approval.task.status_changed_v4) + optional/multi subscription_type pre-registration + user-auth subscription lifecycle + flat output field reference |
| IM | references/lark-event-im.md |
Catalog of 12 IM EventKeys + shape notes (flat vs V2 envelope) + im.message.receive_v1 field gotchas (sender_id is open_id only; .content is plain text except for interactive cards) + common jq recipes (filter by chat_type / message_type / sender); for card.action.trigger see also ../lark-im/references/lark-im-card-action-reply.md |
| Task | references/lark-event-task.md |
Catalog of 1 Task EventKey (task.task.update_user_access_v2) + Native V2 envelope shape + task commit types + user/bot subscription notes |
| VC | references/lark-event-vc.md |
Catalog of 7 VC EventKeys (meeting lifecycle participant_meeting_started/joined/ended_v1, vc.note.generated_v1, recording recording_started/transcript_generated/ended_v1) + field reference + source type semantics; the live list is always lark-cli event list --domain vc --json |
| Minutes | references/lark-event-minutes.md |
Catalog of 1 Minutes EventKey (minutes.minute.generated_v1) + field reference + source type semantics (meeting only) |
| Whiteboard | references/lark-event-whiteboard.md |
Catalog of 1 Board EventKey (board.whiteboard.updated_v1) + per-whiteboard subscription model (requires -p whiteboard_id=<token>) + payload field reference (whiteboard_id / operator_ids triple-id) |
| 1 | |
| 2 | name lark-event |
| 3 | version 1.0.0 |
| 4 | description "Lark/Feishu real-time event listening / subscribing / consuming: stream events as NDJSON via `lark-cli event consume <EventKey>` (covers IM messages/reactions/chat changes, Approval status changes, Task updates, VC meeting started/joined/ended, Minutes generated, Whiteboard updated, etc.). Use for Lark bots, real-time message processing, long-running subscribers, streaming webhook/push handlers. Supports `--max-events` / `--timeout` bounded runs and a stderr ready-marker contract — designed for AI agents running as subprocesses." |
| 5 | metadata |
| 6 | requires |
| 7 | bins ["lark-cli"] |
| 8 | cliHelp "lark-cli event --help" |
| 9 | |
| 10 | |
| 11 | # Lark Events |
| 12 | |
| 13 | > **Prerequisite:** Read [`../lark-shared/SKILL.md`] first for authentication, `--as user/bot` switching, `Permission denied` handling, and safety rules. |
| 14 | |
| 15 | ## Core commands |
| 16 | |
| 17 | | Command | Purpose | |
| 18 | |------|------| |
| 19 | | `lark-cli event list [--json]` | List all subscribable EventKeys | |
| 20 | | `lark-cli event schema <EventKey> [--json]` | Show an EventKey's params and output schema | |
| 21 | | `lark-cli event consume <EventKey> [flags]` | Blocking consume; events → stdout NDJSON | |
| 22 | | `lark-cli event status [--json] [--fail-on-orphan]` | Inspect the local bus daemon status | |
| 23 | | `lark-cli event stop [--all] [--force]` | Stop the bus daemon | |
| 24 | |
| 25 | |
| 26 | ## Common flags |
| 27 | |
| 28 | | Flag | Description | |
| 29 | |---|---| |
| 30 | | `--param key=value` / `-p` | Business params (repeatable; comma-separated for multi-value). Unknown keys fail with valid names listed inline | |
| 31 | | `--jq <expr>` | jq expression to filter / transform each event; empty output skips the event | |
| 32 | | `--max-events N` | Exit after N events. Default 0 = unlimited | |
| 33 | | `--timeout D` | Exit after duration D (e.g. `30s`, `2m`). Default 0 = no timeout. Whichever of `--max-events` / `--timeout` fires first wins | |
| 34 | | `--output-dir <dir>` | Write each event as a file (relative paths only; prevents traversal) | |
| 35 | | `--quiet` | Suppress ready/exit markers and per-event stderr diagnostics, including drop warnings. This can hide event loss. **AI should not use this** — it removes readiness and integrity signals | |
| 36 | | `--as user\|bot\|auto` | Identity for the session (see lark-shared) | |
| 37 | |
| 38 | |
| 39 | ## Examples |
| 40 | |
| 41 | |
| 42 | # Default: stream every event for the key (no filter, no projection) |
| 43 | lark-cli event consume im.message.receive_v1 --as bot |
| 44 | |
| 45 | # List every EventKey of one domain (the authoritative, always-current catalog) |
| 46 | lark-cli event list --domain vc --json |
| 47 | |
| 48 | # Grab one sample event to inspect payload shape |
| 49 | lark-cli event consume im.message.receive_v1 --max-events 1 --timeout 30s --as bot |
| 50 | |
| 51 | # Run for 10 minutes then auto-exit |
| 52 | lark-cli event consume im.message.receive_v1 --timeout 10m --as bot |
| 53 | |
| 54 | # Consume multiple EventKeys concurrently (one shape per process, no dispatcher) |
| 55 | lark-cli event consume im.message.receive_v1 --as bot > receive.ndjson & |
| 56 | lark-cli event consume im.message.reaction.created_v1 --as bot > reaction.ndjson & |
| 57 | wait |
| 58 | |
| 59 | |
| 60 | |
| 61 | ## Call flow |
| 62 | |
| 63 | `lark-cli event list --json` → pick a legal key. `--domain <d>` narrows to one domain; the domains are `application`, `approval`, `board`, `card`, `im`, `minutes`, `task`, `vc`. An unknown domain fails with the valid set listed in the hint. |
| 64 | `lark-cli event schema <key> --json` → read `resolved_output_schema` + `jq_root_path` to determine field paths |
| 65 | `lark-cli event consume <key> [--jq '<expr>']` → consume |
| 66 | |
| 67 | ## Subprocess contract |
| 68 | |
| 69 | ### Ready marker |
| 70 | |
| 71 | `event consume`'s stderr emits a fixed line `[event] ready event_key=<key>`. **Parent processes should block on stderr until this line appears, then start reading stdout.** Do not fall back to `sleep`. |
| 72 | |
| 73 | ### stdin EOF = graceful exit |
| 74 | |
| 75 | `event consume` treats stdin close as a shutdown signal (wired for AI subprocess callers). **Bounded runs are exempt: when `--max-events` or `--timeout` is set (> 0), stdin EOF is ignored and the run exits only via its own bound, timeout, or SIGTERM.** For unbounded runs, `< /dev/null` / `nohup` / systemd's default `StandardInput=null` will cause an immediate graceful exit (stderr `reason: signal`). To keep an unbounded run alive: |
| 76 | |
| 77 | Feed stdin a source that never EOFs: `< <(tail -f /dev/null)` |
| 78 | Or run bounded: `--max-events N` / `--timeout D` |
| 79 | |
| 80 | ### Exit codes & reason |
| 81 | |
| 82 | On exit, the last stderr line is `[event] exited — received N event(s) in Xs (reason: ...)`. |
| 83 | |
| 84 | | exit code | reason | Trigger | |
| 85 | |---|---|---| |
| 86 | | 0 | `reason: limit` | `--max-events` reached | |
| 87 | | 0 | `reason: timeout` | `--timeout` reached | |
| 88 | | 0 | `reason: signal` | Ctrl+C / SIGTERM / stdin EOF (stdin EOF applies to unbounded runs only) | |
| 89 | | 1 | JSON error envelope on stderr | Lark API business failure during pre-consume setup (for example subscription create/delete) | |
| 90 | | 2 | JSON error envelope on stderr (no `exited` line) | Validation failure (unknown EventKey, bad `--param` / `--jq`, another bus already connected) | |
| 91 | | 3 | JSON error envelope on stderr | Auth failure (missing token, missing scopes) | |
| 92 | | 4 / 5 | JSON error envelope on stderr | Network / internal failure (bus startup, handshake, file I/O) | |
| 93 | |
| 94 | Startup and runtime failures emit a structured JSON envelope on stderr: `{"ok":false,"error":{"type","subtype","param","message","hint",...}}` (the envelope may also carry top-level `identity` / `_notice` siblings). Parse `error.type` / `error.subtype` to branch (e.g. `missing_scope` carries a `missing_scopes` list), `error.param` to find the offending flag, and `error.hint` for the recovery action — do not regex-match message text. |
| 95 | |
| 96 | Orchestrators should treat `reason: limit/timeout/signal` (all exit 0) as "business completion" and non-zero as "failure". |
| 97 | |
| 98 | ### Never `kill -9` |
| 99 | |
| 100 | **Avoid `kill -9` on consume processes** for EventKeys whose PreConsume registers a server-side subscription **and** unsubscribes on exit (minutes, vc, board keys): `kill -9` skips the OAPI unsubscribe and leaks the server-side subscription (symptoms: "subscription already exists" on restart, duplicate event delivery). Keys whose subscription is a durable relation with no cleanup (task, approval keys) do not leak this way, but SIGTERM or closing stdin remains the right shutdown for every key. |
| 101 | |
| 102 | ### One consume, one EventKey (multi-key = multi-shell) |
| 103 | |
| 104 | The command takes exactly one positional argument; `k1,k2` and wildcards are unsupported. Listening to N keys means N subprocesses — this is **intentional**: |
| 105 | |
| 106 | One shape per process stdout; no dispatcher logic required in the AI |
| 107 | Fault isolation (one key failing doesn't affect others) |
| 108 | Independent `--as` / `--jq` / `--max-events` / `--timeout` per key |
| 109 | |
| 110 | All N consumers share a single bus daemon (UDS local IPC), so the overhead is small |
| 111 | |
| 112 | ## Writing jq via schema |
| 113 | |
| 114 | `event schema <key> --json` is the source of truth for writing `--jq`. Four things to look at: |
| 115 | |
| 116 | **(1) Where fields start** — see `jq_root_path` |
| 117 | |
| 118 | Value `"."` → fields are at the top level, write `.chat_id` |
| 119 | Value `".event"` → fields are inside a V2 envelope, write `.event.chat_id` |
| 120 | |
| 121 | **(2) Field list and types** — see `resolved_output_schema.properties.<name>` |
| 122 | |
| 123 | Each field carries `type` / `description`, and some also have `format`. Snippet (from `event schema im.message.receive_v1 --json`): |
| 124 | |
| 125 | |
| 126 | { |
| 127 | "chat_id": {"type":"string", "format":"chat_id", "description":"Chat ID, prefixed with oc_"}, |
| 128 | "sender_id": {"type":"string", "format":"open_id", "description":"Sender open_id, prefixed with ou_"}, |
| 129 | "create_time": {"type":"string", "format":"timestamp_ms", "description":"Send time as ms-epoch string"} |
| 130 | } |
| 131 | |
| 132 | |
| 133 | **(3) Field semantics** — see the `format` tag |
| 134 | |
| 135 | Lark-defined semantic tags (**not** JSON Schema's standard `format`). Common values: `open_id` / `chat_id` / `message_id` / `timestamp_ms` / `email`. Purpose: distinguish "same string type, different meanings" fields so you can reverse-lookup via API or convert formats. |
| 136 | |
| 137 | **(4) Decoded state** — read the field's `description` |
| 138 | |
| 139 | `event consume` runs Process hooks that may pre-decode some payload fields (flattening V2 envelopes, rendering `.content` to plain text, etc.) — behavior differs from raw OAPI. **Always read the field's `description` before writing jq**, especially for generic field names like `content` / `data` / `body` / `payload`. |
| 140 | |
| 141 | **Why it matters**: blindly applying `fromjson` to an already-decoded text field makes jq error on every event and silently drop it — the consumer looks alive but emits nothing, with only a single `WARN` line buried on stderr. (This is the general behavior: any jq runtime error skips the event with a one-line WARN; the loop does not abort.) |
| 142 | |
| 143 | **Don't shortcut the schema**: when projecting `event schema --json` with jq, do not strip `.description` from `properties` — that's the field that tells you whether a field is already decoded. Dump the full property objects, not just keys. |
| 144 | |
| 145 | |
| 146 | |
| 147 | **Aside**: `--param`'s valid parameters also live in the schema — the `params` section lists `name` / `type` / `required` / `enum` / `default` / `description`; **section missing = this key accepts no `--param`**. |
| 148 | |
| 149 | ## Topic index |
| 150 | |
| 151 | | Topic | Reference | Coverage | |
| 152 | |------------|------------------------------------------------------------------------------|---| |
| 153 | | Application | [`references/lark-event-application.md`] | Catalog of Application EventKeys, including `application.bot.menu_v6` for custom bot menu push events + flattened `event_key` / operator fields + jq recipe | |
| 154 | | Approval | [`references/lark-event-approval.md`] | Catalog of 2 Approval EventKeys (`approval.instance.status_changed_v4`, `approval.task.status_changed_v4`) + optional/multi `subscription_type` pre-registration + user-auth subscription lifecycle + flat output field reference | |
| 155 | | IM | [`references/lark-event-im.md`] | Catalog of 12 IM EventKeys + shape notes (flat vs V2 envelope) + `im.message.receive_v1` field gotchas (`sender_id` is open_id only; `.content` is plain text except for `interactive` cards) + common jq recipes (filter by chat_type / message_type / sender); for `card.action.trigger` see also [`../lark-im/references/lark-im-card-action-reply.md`] | |
| 156 | | Task | [`references/lark-event-task.md`] | Catalog of 1 Task EventKey (`task.task.update_user_access_v2`) + Native V2 envelope shape + task commit types + user/bot subscription notes | |
| 157 | | VC | [`references/lark-event-vc.md`] | Catalog of 7 VC EventKeys (meeting lifecycle `participant_meeting_started/joined/ended_v1`, `vc.note.generated_v1`, recording `recording_started/transcript_generated/ended_v1`) + field reference + source type semantics; the live list is always `lark-cli event list --domain vc --json` | |
| 158 | | Minutes | [`references/lark-event-minutes.md`] | Catalog of 1 Minutes EventKey (`minutes.minute.generated_v1`) + field reference + source type semantics (meeting only) | |
| 159 | | Whiteboard | [`references/lark-event-whiteboard.md`] | Catalog of 1 Board EventKey (`board.whiteboard.updated_v1`) + per-whiteboard subscription model (requires `-p whiteboard_id=<token>`) + payload field reference (whiteboard_id / operator_ids triple-id) | |
| 160 |
Discussion
Alternatives
Browse more free Claude skills or everything in Development.