Communitygithub.com

Aquarius-mu/skillManager

A personal skill library: collect, organize, and version-manage AI Agent skills.

What is skillManager?

This skill provides a comprehensive reference for building interactive cards on the Feishu (Lark) platform using JSON Schema 2.0. It covers the complete card structure including header, body, and config fields, along with detailed component specifications for `div`, `img`, `table`, `column_set`, `collapsible_panel`, `button`, `input`, and more. The skill emphasizes key rules such as using `body.elements[]` instead of top-level `elements[]`, sending cards with `--as bot` and `--msg-type interactive`, and handling `@` mentions with `<at id="ou_xxx">`. It also includes a complete emoji key reference that must follow the official Feishu list exactly, a detailed Feishu color enumeration table, and practical code snippets for sending, replying, updating, and replacing cards. This is an essential toolkit for any AI agent developer building rich Feishu bot experiences.

Works with✓Claude Code~Codex CLI~Cursor
npx skills add Aquarius-mu/skillManager

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Preview

From the skill README

Hover text

Documentation

feishu-card (schema 2.0)

Key Rules

  • All cards use schema 2.0, elements go inside body.elements[], not top-level elements[]
  • Sending a card must use --as bot, --msg-type interactive, and --content with 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 action container; buttons go directly into body.elements, interaction uses the behaviors array
  • 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 note tag is no longer supported in schema 2.0 (error 200861); for notes, use div + 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

FieldTypeDefaultDescription
streaming_modeboolfalseStreaming update state (for "thinking" placeholder)
enable_forwardbooltrueAllow forwarding the card
update_multiboolfalseJSON 2.0 shared card mode (set true for multi-user sync update)
width_modestringdefault (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 SeriesLight (recommended for backgrounds)MediumDark (recommended for text)
Blueblue-50 blue-100 blue-200blue-300~blue-500blue-600~blue-900
Wathetwathet-50 wathet-100 wathet-200wathet-300~wathet-500wathet-600~wathet-900
Turquoiseturquoise-50 turquoise-100turquoise-300~turquoise-500turquoise-600~turquoise-900
Greengreen-50 green-100 green-200green-300~green-500green-600~green-900
Limelime-50 lime-100lime-300~lime-500lime-600~lime-900
Yellowyellow-50 yellow-100yellow-300~yellow-500yellow-600~yellow-900
Orangeorange-50 orange-100orange-300~orange-500orange-600~orange-900
Redred-50 red-100red-300~red-500red-600~red-900
Carminecarmine-50 carmine-100carmine-300~carmine-500carmine-600~carmine-900
Purplepurple-50 purple-100purple-300~purple-500purple-600~purple-900
Violetviolet-50 violet-100violet-300~violet-500violet-600~violet-900
Indigoindigo-50 indigo-100indigo-300~indigo-500indigo-600~indigo-900
Greygrey-00 grey-50 grey-100grey-300~grey-500grey-600~grey-1000
Whitewhite 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

tagTypeDescription
divContentText block (plain_text / lark_md), supports prefix icon
markdownContentPlain Markdown text (does not support @; use div+lark_md for @)
imgContentImage
tableContentData table (max 5 per card, cannot be nested)
hrLayoutDivider line
noteLayout❌ Deprecated in schema 2.0, use div + lark_md + <font color=grey> instead
column_setContainerMulti-column layout
collapsible_panelContainerCollapsible panel
buttonInteractiveButton (JSON 2.0: place directly in elements, no action container)
inputInteractiveText input (must be used within a form)
select_staticInteractiveStatic 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 action container; buttons go directly into body.elements or column.elements; interaction uses behaviors instead of the old value


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)

![Hover text](img_xxx)

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

  1. Only use keys from the official list; unreleased keys (e.g., :WAITING:, :PROCESSING:) will be rendered as plain text and are forbidden.
  2. Key case must exactly match the official list (e.g., Fire not FIRE, Trophy not TROPHY, Shrug not SHRUG).
  3. <text_tag> does not render :KEY:; use Unicode emoji instead (e.g., ✅).
  4. :KEY: only works in lark_md regular 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]

Related Skills