9.3 KiB
card.action.trigger
Prerequisite: Read
../../lark-event/SKILL.mdfirst forevent consumeessentials.
Fires when a user interacts with an interactive card — button click, form submit, dropdown select, checkbox toggle, date/time pick, etc.
Setup (required)
Console configuration required: In the Feishu Developer Console, go to App → Events & Callbacks → Callback Configuration (应用--事件与回调--回调配置) and enable it. The consumer starts without errors even when not configured, but no events will be received. There is no preflight check for this setting.
After enabling, events are delivered over the existing WebSocket long connection — no additional URL configuration needed.
Scopes & auth
| Scope | Required for |
|---|---|
im:message:readonly |
Auto-fetch card_content via message get API (covers both p2p and group messages) |
Auth: bot only.
Output fields
| Field | Type | Description |
|---|---|---|
type |
string | Always card.action.trigger |
event_id |
string | Unique event ID; safe for deduplication |
timestamp |
string (timestamp_ms) | Event delivery time (ms since epoch) |
operator_id |
string (open_id) | Open ID of the user who interacted |
message_id |
string (message_id) | Message ID of the card (om_xxx) |
chat_id |
string (chat_id) | Chat ID (oc_xxx) |
host |
string | im_message (chat card) or im_top_notice (top banner) |
token |
string | Delayed-update token; valid 30 min, max 2 uses |
action_tag |
string | Component type that was triggered (see decision table) |
action_value |
string | Developer-defined value on the component; serialized to JSON string |
action_name |
string | name attribute of the component |
timezone |
string | User timezone, e.g. Asia/Shanghai; only populated for date/time picker interactions |
form_value |
string (JSON) | All form field values as JSON string, keyed by component name; only present when a button inside a form container is clicked |
input_value |
string | Input text; only for standalone input components (not inside a form) |
option |
string | Selected value for standalone single-select: select_static, select_person, overflow, date_picker, picker_time, picker_datetime |
options |
string | Comma-separated selected values for standalone multi-select: multi_select_static, multi_select_person |
checked |
bool | Checkbox state for standalone checker elements |
card_content |
string | Original card content (userDSL text format) from when the card was sent; auto-fetched via message get API at consume time; empty if message_id absent or fetch fails — skip if empty |
card_content — what it is and how to use it
card_content is the user_dsl field extracted from the card message content, auto-fetched
at event consume time. It represents the card's original definition — use it as the starting
point to understand the current card structure and construct the updated card JSON.
No extra API call is needed — the consumer fetches it automatically. If empty, skip — no fallback required.
action_tag decision table
Form container rule: when a component is inside a
formcontainer, its value appears inform_value[name]instead of the standalone fields (option,options,input_value,checked). There is noform_submittag — form submission comes through asbuttonwithform_valuepopulated.
action_tag |
Read field(s) | Notes |
|---|---|---|
button |
action_value (fromjson if object); form_value if inside a form |
Most common; form_value non-empty = form submit |
overflow |
option |
Collapsible button group selection |
select_static |
option (standalone) or form_value[name] (in form) |
Single-select dropdown |
multi_select_static |
options (standalone) or form_value[name] (in form) |
Multi-select dropdown |
select_person |
option — open_id of selected user |
Single-select person |
multi_select_person |
options — comma-separated open_ids |
Multi-select person |
input |
input_value (standalone) or form_value[name] (in form) |
Text input |
checker |
checked (standalone) or form_value[name] (in form) |
Checkbox |
date_picker |
option (date string) + timezone |
e.g. "2024-04-01 +0800" |
picker_time |
option (time string) + timezone |
e.g. "08:30 +0800" |
picker_datetime |
option (datetime string) + timezone |
e.g. "2024-04-29 07:07 +0800" |
select_img |
option (single) or options (multi) |
Image picker |
Key constraints
- Token valid 30 minutes, max 2 uses — if update fails after exhaustion, inform the user
- Delayed-update API requires complete new card JSON — partial updates are not supported
- SDK auto-responds
{"code":200}within 3 s — your update call can be sent any time within 30 min card_contentis auto-populated — no extra API call needed; if empty, skip it
After starting the listener
Once the listener is running, check whether your agent runtime supports background event monitoring (i.e. can receive and process stdout lines from a running subprocess while continuing to respond to the user). If it does, prompt the user:
"Card callback listener is now active. Do you want me to automatically handle card interactions and update the card based on user actions?"
Only enter the auto-update workflow below if the user confirms. If your runtime does not support background monitoring, inform the user that automatic card updates are not available and they will need to handle interactions manually.
Agent workflow
When a card.action.trigger event arrives (each stdout JSON line is one event — process it immediately):
1. Read action fields to understand what the user did:
- action_tag: which component was triggered
- action_value / option / options / checked / input_value / form_value: what value was set
2. Decide: does this interaction require a card update?
- e.g. button click with a business action → yes
- e.g. navigation / pagination → no (just record, no update needed)
- Not every callback requires a card update — decide based on business semantics
- Before updating, explicitly state what visual change the action requires. If you cannot articulate one, skip the update.
3. If update is needed:
a. If card_content is empty: inform the user that the original card could not be fetched,
so it is not possible to determine whether an update is needed — do not guess
b. Determine the new card state based on the action
c. Use card_content as the structural basis to construct the updated card JSON
d. Detect card version: if card_content contains `"schema":"2.0"` or `"schema": "2.0"` it is Card 2.0; otherwise assume Card 1.0
e. For Card 1.0: include `"open_ids": ["<operator_id>"]` inside the `card` object, or the API returns code 300090
f. Call the delayed update API with the token and new card JSON
4. If no update: end (the SDK has already acknowledged the callback)
Updating the card
lark-cli api POST /open-apis/interactive/v1/card/update --as bot \
--data '{"token":"<token>","card":<new_card_json>}'
--data parameters:
| Field | Required | Description |
|---|---|---|
token |
Yes | Delayed-update token from the event |
card |
Yes | Complete new card JSON — construct based on card_content from the event, modified to reflect the new state |
card.open_ids |
No | Card 1.0 only. Array of open_ids defining which users see the updated card. Must contain at least one open_id (e.g. the operator's); passing [] or omitting the key both cause "openid empty" (code 300090). |
Examples
# Stream all card interactions
lark-cli event consume card.action.trigger --as bot
# Grab one callback to inspect shape (debugging only — do not use in production workflows)
lark-cli event consume card.action.trigger --as bot --max-events 1 --timeout 60s
# Button clicks only (not form submit), with action value
lark-cli event consume card.action.trigger --as bot \
--jq 'select(.action_tag == "button" and .form_value == "") | {op: .operator_id, val: (.action_value | fromjson?), token: .token}'
# Form submits (button with form_value present)
lark-cli event consume card.action.trigger --as bot \
--jq 'select(.action_tag == "button" and .form_value != "") | {op: .operator_id, form: (.form_value | fromjson), token: .token}'
# Date picker interactions
lark-cli event consume card.action.trigger --as bot \
--jq 'select(.action_tag == "date_picker") | {op: .operator_id, date: .option, tz: .timezone}'
# Filter to one chat
lark-cli event consume card.action.trigger --as bot \
--jq 'select(.chat_id == "oc_xxx")'
Gotchas
- No
form_submittag: form submission comes asaction_tag = "button"withform_valuepopulated. Checkform_value != ""to distinguish from a standalone button click. action_valuetype is developer-defined: the original may be an object or a plain string. Usefromjson?(with?to swallow errors) or check before parsing.- Standalone vs form fields:
input_value,option,options,checkedare only populated for components not inside a form container. Inside a form, all values appear inform_value. - WebSocket delivery: no separate callback URL needed; uses the existing WS connection.