18 KiB
im +messages-send
Prerequisite: Read
../lark-shared/SKILL.mdfirst to understand authentication, global parameters, and safety rules.
Send a message to a group chat or a direct message conversation. Supports both user identity (--as user) and bot identity (--as bot).
This skill maps to the shortcut: lark-cli im +messages-send (internally calls POST /open-apis/im/v1/messages).
Safety Constraints
Messages sent by this tool are visible to other people. Before calling it, you must confirm with the user:
- The recipient (which person or which group)
- The message content
- The sending identity (user or bot)
Do not send messages without explicit user approval.
When using --as bot, the message is sent in the app's name, so make sure the app has already been added to the target chat.
When using --as user, the message is sent as the authorized end user and requires the im:message.send_as_user and im:message scopes.
Choose The Right Content Flag
Default Selection Rule For Agents
- Prefer
--markdownfor headings, lists, links, summaries, reports, or Markdown-looking content. - Use
--textfor exact plain text: logs, code, indentation-sensitive text, or literal Markdown. - Use
--contentfor exactpostJSON, titles, multiple locales, cards, or unsupported structures.
| Need | Recommended flag | Why |
|---|---|---|
| Send headings, lists, links, summaries, or reports | --markdown |
Best default for lightweight formatting; converted to Feishu post JSON |
| Send plain text exactly as written | --text |
Preserves literal text; no Markdown conversion |
| Precisely control the final payload | --content |
You provide the exact JSON for text / post / interactive / share_* / media payloads |
| Send image / file / video / audio | --image / --file / --video / --audio |
Shortcut uploads URLs, or cwd-relative local files automatically |
--text vs --markdown
- Use
--markdownfor lightweight formatted messages. - Use
--textfor exact plain text, especially logs, code, indentation, or Markdown characters that should not render. - Use
--contentwhen--markdownis not enough, especially if you need exactpostJSON, a title, multiple locales, cards, or unsupported rich structures.
What --markdown Really Does
--markdown accepts Markdown-like input and converts it to the Feishu post payload required by the message API.
The shortcut does all of the following before sending:
- Forces
msg_type=post - Resolves remote Markdown images like
by downloading and uploading them first - Normalizes the Markdown for Feishu post rendering
- Wraps the result as:
{"zh_cn":{"content":[[{"tag":"md","text":"..."}]]}}
This makes --markdown the simplest path for lightweight formatted messages.
Markdown Boundaries
- It does not promise full CommonMark / GitHub Flavored Markdown support.
- It always becomes a
postpayload with a singlezh_cnlocale. - It does not let you set a
posttitle. If you need a title, use--msg-type post --content .... - Headings are rewritten:
# Titlebecomes#### Title##to######are normalized to#####when the content contains H1-H3
- Consecutive headings are separated with blank lines after heading normalization.
- Block spacing and line breaks may be normalized during conversion.
- Code blocks are preserved as code blocks.
- Excess blank lines are compressed.
- Already-uploaded
img_xxximage keys are the most reliable Markdown image input. - Local paths in Markdown image syntax like
are not supported and will not be auto-uploaded. - Remote URLs (
https://...) will be auto-downloaded and uploaded at runtime; if the download or upload fails, the image is removed with a warning.
If you need a title, multiple locales, cards, unsupported rich structures, or byte-for-byte post JSON control, use --content and provide the final JSON yourself.
Image Constraint for --markdown
When using --markdown with images, prefer pre-uploading via images.create and referencing  for predictable results. Remote URLs may work but are not guaranteed.
Steps:
# 1. Upload image to get image_key
lark-cli im images create --data '{"image_type":"message"}' --file ./diagram.png
# Returns: {"image_key":"img_v3_xxxx"}
# 2. Use image_key in --markdown
lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Report\n\n\n\nSee above for details.'
Preserving Formatting
If the message has multiple lines, indentation, code blocks, tabs, or many quotes/backslashes, prefer shell ANSI-C quoting with $'...' for either --markdown or --text.
This is especially useful in zsh / bash because it lets you write \n explicitly instead of relying on the shell to preserve literal newlines.
When formatting must be preserved
Use --text plus $'...':
lark-cli im +messages-send --chat-id oc_xxx --text $'Build failed\nBranch: feature/im-docs\nAction: please check logs'
lark-cli im +messages-send --chat-id oc_xxx --text $'```bash\nmake test\nmake lint\n```'
Use this path when you want the receiver to see the text exactly as entered, not a converted Markdown post.
Commands
# Send a formatted update
lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Update\n\n- item 1\n- item 2'
# Send a plain one-line message
lark-cli im +messages-send --chat-id oc_xxx --text "Hello"
# Equivalent manual JSON
lark-cli im +messages-send --chat-id oc_xxx --content '{"text":"Hello"}'
# Send to a direct message (pass open_id)
lark-cli im +messages-send --user-id ou_xxx --text "Hello"
# Send multi-line text while preserving formatting
lark-cli im +messages-send --chat-id oc_xxx --text $'Line 1\nLine 2\n indented line'
# Send Markdown with an image (must pre-upload via images.create)
lark-cli im images create --data '{"image_type":"message"}' --file ./screenshot.png
# Use the returned image_key in the markdown content
lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Status\n\n\n\nDone.'
# If you need exact post structure, send JSON directly
lark-cli im +messages-send --chat-id oc_xxx --msg-type post --content '{"zh_cn":{"title":"Title","content":[[{"tag":"text","text":"Body"}]]}}'
# Send a local image (uploaded automatically before sending)
lark-cli im +messages-send --chat-id oc_xxx --image ./photo.png
# Or send directly with an existing image_key
lark-cli im +messages-send --chat-id oc_xxx --image img_xxx
# Send a local file (uploaded automatically before sending)
lark-cli im +messages-send --chat-id oc_xxx --file ./report.pdf
# Send a video (--video-cover is required as the cover)
lark-cli im +messages-send --chat-id oc_xxx --video ./demo.mp4 --video-cover ./cover.png
lark-cli im +messages-send --chat-id oc_xxx --video ./demo.mp4 --video-cover img_xxx
# Send a voice message
lark-cli im +messages-send --chat-id oc_xxx --audio ./voice.opus
# Use an idempotency key (same key sends only once within 1 hour)
lark-cli im +messages-send --chat-id oc_xxx --text "Hello" --idempotency-key my-unique-id
# Preview the request without executing it
lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Test\n\nhello' --dry-run
# ===== Interactive Card =====
# 🚫 STOP — before constructing ANY interactive card JSON, you MUST read
# card/lark-im-card-create.md and follow its workflow. Do NOT
# hand-write or copy a card payload from the examples below. The JSON passed
# to --content must be the OUTPUT of that workflow. This is non-negotiable.
# Once the workflow has produced the card JSON, send it:
lark-cli im +messages-send --chat-id oc_xxx --msg-type interactive --content '<card_json_from_workflow>'
Media Input Rules
- Media flags accept an existing key (
img_xxx/file_xxx), anhttp://orhttps://URL, or a local file path. - Local paths must be relative to the current working directory and stay within it after resolving
..and symlinks. - Absolute paths such as
/tmp/photo.pngare rejected. Run the command from the file's directory and pass./photo.png, or copy the file into the current directory first. --audiosends a voice message and accepts only Opus audio (.opusor Ogg Opus.ogg) for local paths and URLs. Formp3,wav, or other non-Opus audio, convert to.opusbefore using--audio, or use--fileto send the original audio as an attachment.
Parameters
| Parameter | Required | Description |
|---|---|---|
--chat-id <id> |
One of two | Group chat ID (oc_xxx) |
--user-id <id> |
One of two | User open_id (ou_xxx) for direct messages |
--text <string> |
One content option | Plain text message. Use when exact text and formatting preservation matter. Automatically wrapped as {"text":"..."} |
--markdown <string> |
One content option | Best default for lightweight formatted messages such as headings, lists, links, summaries, and reports. Internally converted to post JSON with Feishu-specific normalization |
--content <json> |
One content option | Exact message content JSON string; use this when you need full control over msg_type and payload. The JSON must match the effective --msg-type |
--image <path|url|key> |
One content option | Cwd-relative local image path, URL, or image_key (img_xxx). Local paths and URLs are uploaded automatically |
--file <path|url|key> |
One content option | Cwd-relative local file path, URL, or file_key (file_xxx). Local paths and URLs are uploaded automatically |
--video <path|url|key> |
One content option | Cwd-relative local video path, URL, or file_key (file_xxx). Local paths and URLs are uploaded automatically. Must be paired with --video-cover |
--video-cover <path|url|key> |
Required with --video |
Cwd-relative local cover image path, URL, or image_key (img_xxx). Local paths and URLs are uploaded automatically |
--audio <path|url|key> |
One content option | Voice-message audio key, URL, or cwd-relative local path. Local paths and URLs must be Opus (.opus or Ogg Opus .ogg) |
--msg-type <type> |
No | Message type (default text). If you use --text / --markdown / media flags, the effective type is inferred automatically. Explicitly setting a conflicting --msg-type fails validation |
--idempotency-key <key> |
No | Idempotency key, max 50 characters; the same key sends only one message within 1 hour |
--as <identity> |
No | Identity type: bot or user (default bot) |
--dry-run |
No | Print the request only, do not execute it |
Mutual exclusivity rule:
--text,--markdown,--content, and--image/--file/--video/--audiocannot be used together. Media flags are also mutually exclusive with each other.Video cover rule:
--videomust be accompanied by--video-cover. Omitting--video-coverwhen using--videowill fail validation.--video-covercannot be used without--video.
Common Mistakes
- Choosing
--textfor headings, lists, links, summaries, or reports. Use--markdown. - Choosing
--markdownwhen you actually need exact plain text. If exact line breaks, spacing, logs, code, or literal Markdown characters matter, use--text, usually with$'...'. - Assuming
--markdownsupports every Markdown feature. It is converted into a Feishupostpayload and normalized first. - Putting local image paths inside Markdown like
.--markdowndoes not auto-upload those paths. - Using local file paths inside Markdown image syntax (e.g.
) with--markdown. Local paths are not auto-uploaded and will not render as an image. Pre-upload viaimages.createto get animage_keyinstead. - Using
--contentwithout making the JSON match the effective--msg-type. - Explicitly setting
--msg-typeto something that conflicts with--text,--markdown, or media flags. - Mixing
--text,--markdown, or--contentwith media flags in one command.
content Format Reference
msg_type |
Example content |
|---|---|
text |
{"text":"Hello <at user_id=\"ou_xxx\">name</at>"} |
post |
{"zh_cn":{"title":"Title","content":[[{"tag":"text","text":"Body"}]]}} |
image |
{"image_key":"img_xxx"} |
file |
{"file_key":"file_xxx"} |
audio |
{"file_key":"file_xxx"} |
media |
{"file_key":"file_xxx","image_key":"img_xxx"} (video; image_key is the cover from --video-cover — required) |
share_chat |
{"chat_id":"oc_xxx"} |
share_user |
{"user_id":"ou_xxx"} |
interactive |
Card JSON — MUST be produced by the card/lark-im-card-create.md workflow. Read it before writing any card; never hand-craft the JSON here |
postvsinteractive:postis a static rich-text message (title, paragraphs, @mentions, links, inline images) — content is fixed once sent.interactiveis a card with structured layout and UI components (buttons, forms, selects, date pickers, charts) — content can be updated after sending and supports user-action callbacks. Usepostfor read-only content; useinteractivewhen the message needs user interaction or dynamic updates.
interactive cards support callback events (card.action.trigger) — see lark-im-card-action-reply.md.
Return Value
{
"message_id": "om_xxx",
"chat_id": "oc_xxx",
"create_time": "1234567890"
}
@Mention Format
The <at> syntax differs by message type. The shortcut only normalizes mentions for text and post; interactive card content is passed through verbatim, so cards must use the card-native syntax below.
text
<at user_id="ou_xxx">name</at>— the inner text is the mentioned user's display name and is optional (<at user_id="ou_xxx"></at>also works)- @all:
<at user_id="all"></at>
post
- Inside a
textormdelement, the same inline form astextworks:<at user_id="ou_xxx">name</at> - Or use a dedicated
atelement node:{"tag":"at","user_id":"ou_xxx"}(use"all"to mention everyone)
interactive (card)
Card content is not normalized — use the card-native <at> syntax inside a lark_md / markdown element:
- single user by open_id:
<at id=ou_xxx></at> - multiple users:
<at ids=ou_xxx1,ou_xxx2></at> - by email:
<at email=user@example.com></at>
Notes
--chat-idand--user-idare mutually exclusive; you must provide exactly one--contentmust be valid JSON- When using
--content, you are responsible for making the JSON structure match the effectivemsg_type --image/--file/--video/--audiosupport existing keys, URLs, and cwd-relative local file paths; the shortcut uploads local paths and URLs first, then sends the message; both the upload and send steps use the same identity (UAT when--as user, TAT when--as bot)- If the provided media value starts with
img_orfile_, it is treated as an existing key and used directly --markdownalways sendsmsg_type=post, even if you do not explicitly set--msg-type post- If you explicitly set
--msg-typeand it conflicts with the chosen content flag, validation fails - When using
--video,--video-coveris required as the video cover --dry-runuses placeholder image keys for remote Markdown images and placeholder media keys for local uploads- Failures return an error code and message
--as useruses a user access token (UAT) and requires theim:message.send_as_userandim:messagescopes; the message is sent as the authorized end user--as botuses a tenant access token (TAT) and requires theim:message:send_as_botscope- When sending as a bot, the app must already be in the target group or already have a direct-message relationship with the target user
- When using
--markdownwith images, pre-uploading viaimages.createto obtain animage_keyis recommended for reliability; remote URLs may be auto-resolved at runtime, but if download/upload fails the image is removed with a warning; local paths are not supported - Interactive cards are gated: you MUST read and follow the
card/lark-im-card-create.mdworkflow to produce the card JSON before sending. Do not hand-write or copy a card payload — the JSON given to--msg-type interactive --contentmust be the workflow's output. This applies every time, with no exception