feishu-card (schema 2.0)
Key Rules
- All cards use schema 2.0, elements go inside
body.elements[], not top-levelelements[] - Sending a card must use
--as bot,--msg-type interactive, and--contentwith a JSON string - Mentioning people inside a card uses
<at id="ou_xxx">Name</at>(not<at user_id>) - JSON 2.0 buttons no longer have an
actioncontainer; buttons go directly intobody.elements, interaction uses thebehaviorsarray - First send a "thinking" card to get a
message_id, then PATCH update it in place after Claude responds, to avoid spamming the chat - The
notetag is no longer supported in schema 2.0 (error 200861); for notes, usediv + lark_md(with<font color=grey>Note text</font>) - Building card JSON must use Python heredoc or write to a file; avoid
$(python3 -c "...")— bash will throw syntax errors when the content contains backticks or special characters
Top-Level Structure Complete Fields
{
"schema": "2.0",
"config": {
"streaming_mode": false,
"enable_forward": true,
"update_multi": true,
"width_mode": "default"
},
"card_link": {
"url": "https://...",
"pc_url": "https://...",
"ios_url": "https://...",
"android_url": "https://..."
},
"header": { "...": "..." },
"body": {
"direction": "vertical",
"padding": "0px",
"elements": []
}
}
Config Fields
| Field | Type | Default | Description |
|---|---|---|---|
streaming_mode | bool | false | Streaming update state (for "thinking" placeholder) |
enable_forward | bool | true | Allow forwarding the card |
update_multi | bool | false | JSON 2.0 shared card mode (set true for multi-user sync update) |
width_mode | string | default (600px) | compact (400px) / fill (full-width responsive) / default |
Header Fields
{
"header": {
"title": {"tag": "plain_text", "content": "Title"},
"subtitle": {"tag": "plain_text", "content": "Subtitle (optional)"},
"template": "blue",
"icon": {
"tag": "standard_icon",
"token": "robot-outlined",
"color": "blue"
},
"text_tag_list": [
{"tag": "text_tag", "text": {"tag": "plain_text", "content": "New"}, "color": "blue"}
]
}
}
template colors: blue / wathet / turquoise / green / yellow / orange / red / carmine / violet / purple / indigo / grey
Feishu Color Enumeration Quick Reference
All fields that support color enumeration (background_color, text_color, icon color, etc.) use the following format. Do not use only the base color name (e.g., using "blue" alone in some components will cause an error); include the level suffix:
| Color Series | Light (recommended for backgrounds) | Medium | Dark (recommended for text) |
|---|---|---|---|
| Blue | blue-50 blue-100 blue-200 | blue-300~blue-500 | blue-600~blue-900 |
| Wathet | wathet-50 wathet-100 wathet-200 | wathet-300~wathet-500 | wathet-600~wathet-900 |
| Turquoise | turquoise-50 turquoise-100 | turquoise-300~turquoise-500 | turquoise-600~turquoise-900 |
| Green | green-50 green-100 green-200 | green-300~green-500 | green-600~green-900 |
| Lime | lime-50 lime-100 | lime-300~lime-500 | lime-600~lime-900 |
| Yellow | yellow-50 yellow-100 | yellow-300~yellow-500 | yellow-600~yellow-900 |
| Orange | orange-50 orange-100 | orange-300~orange-500 | orange-600~orange-900 |
| Red | red-50 red-100 | red-300~red-500 | red-600~red-900 |
| Carmine | carmine-50 carmine-100 | carmine-300~carmine-500 | carmine-600~carmine-900 |
| Purple | purple-50 purple-100 | purple-300~purple-500 | purple-600~purple-900 |
| Violet | violet-50 violet-100 | violet-300~violet-500 | violet-600~violet-900 |
| Indigo | indigo-50 indigo-100 | indigo-300~indigo-500 | indigo-600~indigo-900 |
| Grey | grey-00 grey-50 grey-100 | grey-300~grey-500 | grey-600~grey-1000 |
| White | white bg-white | — | — |
Recommended Light Background Colors (for collapsible_panel, column backgrounds, etc.):
blue-100 wathet-100 green-100 yellow-100
orange-100 red-100 purple-100 carmine-100
turquoise-100 violet-100 indigo-100 lime-100
Component Quick Reference
| tag | Type | Description |
|---|---|---|
div | Content | Text block (plain_text / lark_md), supports prefix icon |
markdown | Content | Plain Markdown text (does not support @; use div+lark_md for @) |
img | Content | Image |
table | Content | Data table (max 5 per card, cannot be nested) |
hr | Layout | Divider line |
note | Layout | ❌ Deprecated in schema 2.0, use div + lark_md + <font color=grey> instead |
column_set | Container | Multi-column layout |
collapsible_panel | Container | Collapsible panel |
button | Interactive | Button (JSON 2.0: place directly in elements, no action container) |
input | Interactive | Text input (must be used within a form) |
select_static | Interactive | Static dropdown selector |
div (Text Block)
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": "**Bold** _Italic_ `Code`",
"text_size": "normal",
"text_color": "default",
"text_align": "left",
"lines": 3
},
"icon": {"tag": "standard_icon", "token": "robot-outlined", "color": "blue"}
}
text.tag: plain_text (plain text) or lark_md (Markdown + @)
text_size: heading-0 / heading / normal (default) / notation / x-small
text_align: left / center / right
img (Image)
{
"tag": "img",
"img_key": "img_xxx",
"alt": {"tag": "plain_text", "content": "Hover description"},
"title": {"tag": "plain_text", "content": "Image title"},
"scale_type": "crop_center",
"size": "medium",
"corner_radius": "4px",
"preview": true
}
scale_type: crop_center / crop_top / fit_horizontal
size: stretch (full width) / large / medium / small / tiny / "300px 200px" (custom)
JSON 2.0 does not support
stretch_without_padding; for full-width effect use negative margin
column_set (Multi-Column Layout)
{
"tag": "column_set",
"flex_mode": "bisect",
"horizontal_spacing": "medium",
"background_style": "default",
"columns": [
{
"tag": "column",
"width": "weighted",
"weight": 1,
"vertical_align": "top",
"elements": [{"tag": "div", "text": {"tag": "plain_text", "content": "Left column"}}]
},
{
"tag": "column",
"width": "weighted",
"weight": 1,
"elements": [{"tag": "div", "text": {"tag": "plain_text", "content": "Right column"}}]
}
]
}
flex_mode: none (proportional compression) / stretch (vertical stacking on narrow screens) / flow (auto wrap) / bisect (two equal columns) / trisect (three equal columns)
horizontal_spacing: small (4px) / medium (8px) / large (12px) / extra_large (16px)
column.width: auto / weighted (with weight 1-5) / "200px"
Max nesting depth is 5; form containers and tables cannot be placed inside columns
collapsible_panel (Collapsible Panel)
{
"tag": "collapsible_panel",
"expanded": false,
"background_color": "default",
"header": {
"title": {"tag": "plain_text", "content": "Click to expand"},
"background_color": "grey",
"icon": {"tag": "standard_icon", "token": "arrow-down-outlined"},
"icon_expanded_angle": -180
},
"elements": [
{"tag": "div", "text": {"tag": "plain_text", "content": "Collapsed content"}}
]
}
table (Data Table)
{
"tag": "table",
"page_size": 5,
"row_height": "low",
"freeze_first_column": false,
"header_style": {
"text_align": "left",
"text_size": "normal",
"background_style": "grey",
"bold": true
},
"columns": [
{"name": "name", "display_name": "Name", "data_type": "text", "width": "auto"},
{"name": "status", "display_name": "Status", "data_type": "options", "width": "120px"},
{"name": "count", "display_name": "Count", "data_type": "number",
"format": {"precision": 0, "separator": true}},
{"name": "ts", "display_name": "Time", "data_type": "date",
"format": "YYYY/MM/DD HH:mm"}
],
"rows": [
{"name": "John Doe", "status": [{"name": "In Progress", "color": "blue"}], "count": 42, "ts": 1700000000000}
]
}
data_type: text / lark_md / markdown / options / number / persons / date
row_height: low / middle / high / auto / "48px" (32-124px)
Max 5 tables per card, cannot be nested
button (JSON 2.0)
{
"tag": "button",
"text": {"tag": "plain_text", "content": "Confirm"},
"type": "primary",
"size": "medium",
"width": "default",
"disabled": false,
"behaviors": [
{
"type": "callback",
"value": {"action": "confirm", "data": "payload"}
}
],
"confirm": {
"title": {"tag": "plain_text", "content": "Confirmation"},
"text": {"tag": "plain_text", "content": "Confirm this action?"}
}
}
type: default (grey) / primary (blue) / danger (red) / text / primary_text / danger_text / primary_filled / danger_filled / laser
size: tiny / small / medium / large
behaviors[].type: callback (triggers an event) / open_url (navigate to a link)
JSON 2.0 change: No more
actioncontainer; buttons go directly intobody.elementsorcolumn.elements; interaction usesbehaviorsinstead of the oldvalue
input (Text Input)
{
"tag": "input",
"name": "field_name",
"placeholder": {"tag": "plain_text", "content": "Please enter..."},
"default_value": "",
"input_type": "text",
"max_length": 200,
"required": false,
"disabled": false,
"label": {"tag": "plain_text", "content": "Label"},
"label_position": "top",
"width": "fill"
}
input_type: text / multiline_text / password
User submission triggers a card.action.trigger event, which contains input_value
@mention Syntax
| Message Type | @person syntax | @all |
|---|---|---|
Card interactive lark_md | <at id="ou_xxx">Name</at> | <at id="all">Everyone</at> |
Text text / post | <at user_id="ou_xxx">Name</at> | <at user_id="all"></at> |
Verified working element structure for @mention inside cards (use div + lark_md, not a bare markdown tag):
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": "<at id=\"ou_xxx\">Name</at> message content"
}
}
Markdown Syntax Quick Reference (inside lark_md / markdown tag)
Text Styles
**Bold** *Italic* ~~Strikethrough~~ `Inline Code`
Colored Text
<font color=red>Red</font>
<font color=green>Green</font>
<font color=grey>Grey</font>
<font color=blue>Blue</font>
<font color=orange>Orange</font>
<font color=wathet>Wathet</font>
<font color=purple>Purple</font>
Full colors: red / green / grey / blue / orange / wathet / purple / yellow / turquoise / indigo / violet / lime
Tags / Number Badges
<text_tag color='red'>Tag</text_tag>
<number_tag>99</number_tag>
Localized Time
<local_datetime millisecond='1700000000000' format_type='date_num'></local_datetime>
format_type: date_num (2023/11/15) / date_full (November 15, 2023) / datetime (includes hour, minute, second)
Headings / Lists / Blockquotes
# H1 ## H2 ### H3 #### H4 ##### H5 ###### H6
- Unordered 1. Ordered > Blockquote
Code Blocks
```python
print("hello")
```
Images (inside Markdown)

Tables (max 5 rows of data)
| Col1 | Col2 |
|------|------|
| A | B |
Links
[Text](https://url) [Phone](tel://13800138000)
Dividers / Line Breaks
--- or <hr>
<br/> or two carriage returns (paragraph break)
Emoji
⚠️ Strict Rules
- Only use keys from the official list; unreleased keys (e.g.,
:WAITING:,:PROCESSING:) will be rendered as plain text and are forbidden. - Key case must exactly match the official list (e.g.,
FirenotFIRE,TrophynotTROPHY,ShrugnotSHRUG). <text_tag>does not render:KEY:; use Unicode emoji instead (e.g.,✅).:KEY:only works inlark_mdregular text areas.
Release Notification Common
:DONE: :OK: :THUMBSUP: :APPLAUSE:
:PARTY: :FIREWORKS: :MUSCLE: :THANKS:
:Trophy: :Fire: :Hundred: :CheckMark:
Work & Status
:LGTM: :OnIt: :OneSecond: :Get:
:ERROR: :CrossMark: :Pin: :Alarm:
:Loudspeaker: :OKR: :Yes: :No:
:HEADSET: :Typing: :MeMeMe: :Sigh:
:Lemon: :AWESOMEN: :VRHeadset: :YouAreTheBest:
:SALUTE: :HIGHFIVE: :FISTBUMP: :FINGERHEART:
:WAVE: :ThumbsDown:
Expressions - Positive
:SMILE: :LAUGH: :LOL: :LOVE:
:WINK: :PROUD: :WITTY: :JOYFUL:
:WOW: :BLUSH: :HUG: :BeamingFace:
:Delighted: :ThanksFace: :SaluteFace: :PRAISE:
:CLAP: :JIAYI: :FACEPALM: :YEAH:
Expressions - Negative / Funny
:CRY: :SOB: :ANGRY: :SHOCKED:
:Shrug: :THINKING: :DIZZY: :SLEEP:
:YAWN: :SICK: :SWEAT: :EMBARRASSED:
:SKULL: :RAINBOWPUKE: :PUKE: :ClownFace:
:ColdSweat: :SMUG: :SILENT: :TERROR:
:PETRIFIED: :BETRAYED: :WRONGED:
Celebrations & Holidays
:PARTY: :FIREWORKS: :REDPACKET: :FORTUNE:
:FIRECRACKER: :XmasTree: :XmasHat: :Snowman:
:Pumpkin: :Mooncake: :MoonRabbit: :Partying:
:StickyRiceBalls: :JubilantRabbit: :LUCK:
:FullMoonFace: :GoGoGo: :HappyDragon:
Items & Food
:ROSE: :HEART: :BEER: :CAKE:
:GIFT: :BOMB: :Fire: :Trophy:
:Coffee: :BubbleTea: :Drumstick: :Pepper:
:CUCUMBER: :CANDIEDHAWS: :Soccer: :Basketball:
:Music: :CLEAVER: :EatingFood: :LIPS:
Status Indicators (do not use frequently, only for status scenarios)
:GeneralDoNotDisturb: :GeneralInMeetingBusy:
:StatusReading: :GeneralWorkFromHome:
:GeneralBusinessTrip: :StatusInFlight:
:GeneralSun: :GeneralMoonRest:
:StatusFlashOfInspiration: :Status_PrivateMessage:
:GeneralTravellingCar: :StatusBus:
Unicode Emoji (for titles, inside text_tag, header subtitle)
🎉 ✅ ❌ ⚠️ 🚀 📦 💬 ⏳ 🔔 📝 🛠️ 🔍 👋 🎯 💡 🌟
👥 📅 🤖 🏆 🔥 💯 📣 🎁 ☕ 🍺
Recommended style: Use Unicode emoji in titles for visual enhancement; use Feishu-specific
:DONE:,:LGTM:, etc. for status/feedback.
Sending / Replying / Updating
Send to a Group (New Message)
CARD=$(python3 -c "
import json
card = {
'schema': '2.0',
'header': {'title': {'tag': 'plain_text', 'content': 'Title'}, 'template': 'blue'},
'body': {'elements': [{'tag': 'markdown', 'content': 'Content'}]}
}
print(json.dumps(card))
")
lark-cli im +messages-send \
--chat-id oc_xxx \
--msg-type interactive \
--content "$CARD" \
--as bot
Reply to a Specific Message (Get message_id for future updates)
MSG_ID=$(lark-cli im +messages-reply \
--message-id om_xxx \
--msg-type interactive \
--content "$CARD" \
--as bot \
--jq '.data.message_id')
Reply in a Thread (Doesn't appear in the main chat flow)
lark-cli im +messages-reply \
--message-id om_xxx \
--msg-type interactive \
--content "$CARD" \
--reply-in-thread \
--as bot
Prevent Duplicate Sending (Same key only sends once within 1 hour)
lark-cli im +messages-send \
--chat-id oc_xxx \
--msg-type interactive \
--content "$CARD" \
--idempotency-key "deploy-654.0-notify" \
--as bot
Update Card In Place (PATCH)
PAYLOAD=$(jq -n --argjson card "$CARD" '{"msg_type":"interactive","content":($card|tojson)}')
lark-cli api PATCH "/open-apis/im/v1/messages/${MSG_ID}" \
--data "$PAYLOAD" \
--as bot
Card Templates
Thinking (Streaming state, send a placeholder first)
make_thinking_card() {
local name="$1" question="$2"
jq -n --arg name "$name" --arg q "$question"
...
[truncated for translation]