feishu-doc-write
by @sunnyyao2222-eng
Feishu (Lark) Document API writing spec. Converts Markdown content to Feishu Block structures and writes to cloud docs. Handles concurrency ordering. Use when syncing articles, creating document blocks, or writing long-form content to Feishu docs.
clawhub install feishu-doc-writeπ About This Skill
name: feishu-doc-writer description: Feishu (Lark) Document API writing spec. Converts Markdown content to Feishu Block structures and writes to cloud docs. Handles concurrency ordering. Use when syncing articles, creating document blocks, or writing long-form content to Feishu docs.
Feishu Document Writer
Reference spec for writing content to Feishu (Lark) cloud documents via the Docx API. Feishu docs use a Block tree model β raw Markdown is not accepted.
Document (block_type=1, Page)
+-- Heading1 Block (block_type=3)
+-- Text Block (block_type=2)
+-- Callout Block (block_type=19)
| +-- Text Block
| +-- Bullet Block
+-- Image Block (block_type=27)
+-- Divider Block (block_type=22)
Preferred Approach: Convert API
Feishu provides an official Markdown -> Blocks conversion endpoint:
POST /open-apis/docx/v1/documents/{document_id}/convert
{
"content": "# Title\n\nBody text\n\n- Item 1\n- Item 2\n\n> Quote",
"content_type": "markdown"
}
Pros: No manual Block JSON construction. Handles most standard Markdown. Limitation: Does not support Feishu-specific blocks (Callout, etc.) β use manual Block creation for those.
Block Type Reference
| block_type | Name | JSON Key | Notes |
|-----------|------|----------|-------|
| 1 | Page | page | Document root |
| 2 | Text | text | Paragraph |
| 3-11 | Heading1-9 | heading1-heading9 | Headings |
| 12 | Bullet | bullet | Unordered list (each item = separate block) |
| 13 | Ordered | ordered | Ordered list |
| 14 | Code | code | Code block (with style.language enum) |
| 15 | Quote | quote | Blockquote |
| 17 | Todo | todo | Checkbox item (with style.done) |
| 19 | Callout | callout | Highlight box (Feishu-specific, container block) |
| 22 | Divider | divider | Horizontal rule |
| 27 | Image | image | Two-step: create placeholder, then upload |
| 31 | Table | table | Table |
| 34 | QuoteContainer | quote_container | Quote container |
Create Blocks API
POST /open-apis/docx/v1/documents/{document_id}/blocks/{block_id}/children?document_revision_id=-1Headers:
Content-Type: application/json
Authorization: Bearer
Body:
{
"children": [ ...Block array... ],
"index": 0
}
block_id: Parent block ID (usually document_id itself for root)index: Insert position (0 = beginning, -1 or omit = end)Block JSON Examples
Text
{
"block_type": 2,
"text": {
"elements": [{
"text_run": {
"content": "Paragraph text here",
"text_element_style": { "bold": false, "italic": false }
}
}]
}
}
Heading
{ "block_type": 3, "heading1": { "elements": [{ "text_run": { "content": "H1 Title" } }] } }
{ "block_type": 4, "heading2": { "elements": [{ "text_run": { "content": "H2 Title" } }] } }
Bullet / Ordered List
{ "block_type": 12, "bullet": { "elements": [{ "text_run": { "content": "List item" } }] } }
{ "block_type": 13, "ordered": { "elements": [{ "text_run": { "content": "Numbered item" } }] } }
Each list item is a separate Block.
Code Block
{
"block_type": 14,
"code": {
"elements": [{ "text_run": { "content": "console.log('hello');" } }],
"style": { "language": 23, "wrap": false }
}
}
Common language enums: PlainText=1, JavaScript=23, Python=40, TypeScript=49, Go=20, Shell=46, SQL=47, Java=22, Rust=44, C=12, CSS=17, HTML=21, Docker=19.
Callout (Feishu-specific highlight box)
Callout is a container block β create it first, then add child blocks inside.
// Step 1: Create callout as document child
{ "block_type": 19, "callout": { "background_color": 3, "border_color": 3, "emoji_id": "star" } }// Step 2: POST .../blocks/{callout_block_id}/children
{ "children": [{ "block_type": 2, "text": { "elements": [{ "text_run": { "content": "Highlight text" } }] } }] }
Color enums: Red=1, Orange=2, Yellow=3, Green=4, Blue=5, Purple=6, Grey=7.
Divider
{ "block_type": 22, "divider": {} }
Image (two-step)
Step 1: Create placeholder block { "block_type": 27, "image": {} }
Step 2: Upload via POST /open-apis/drive/v1/medias/upload_all
- multipart/form-data: file, file_name, parent_type="docx_image", parent_node=
Text Styling
Apply styles via text_element_style in text_run:
| Property | Type | Effect |
|----------|------|--------|
| bold | bool | Bold |
| italic | bool | Italic |
| strikethrough | bool | Strikethrough |
| underline | bool | Underline |
| inline_code | bool | Inline code |
| text_color | int | Text color (same enum as callout colors) |
| background_color | int | Background color |
| link.url | string | Hyperlink |
Multiple text_run elements in one block = mixed styles in one paragraph.
Markdown to Block Mapping
| Markdown | block_type | JSON Key |
|----------|-----------|----------|
| # H1 | 3 | heading1 |
| ## H2 | 4 | heading2 |
| ### H3 | 5 | heading3 |
| Paragraph | 2 | text |
| - item | 12 | bullet |
| 1. item | 13 | ordered |
| Code fence | 14 | code |
| > quote | 15 | quote |
| - [ ] todo | 17 | todo |
| --- | 22 | divider |
|  | 27 | image (two-step) |
| bold | -- | text_element_style.bold: true |
| *italic* | -- | text_element_style.italic: true |
| ` code | -- | text_element_style.inline_code: true |
| ~~strike~~ | -- | text_element_style.strikethrough: true |
| text | -- | text_element_style.link.url |
| (no MD equivalent) | 19 | callout (Feishu-specific) |
Concurrency & Ordering (Critical)
Problem: Concurrent Block creation API calls produce random ordering.
Solution A: Single Batch Request (Recommended)
Put all blocks in one children array, single API call:
{
"children": [
{ "block_type": 3, "heading1": { "elements": [{"text_run": {"content": "Title"}}] } },
{ "block_type": 2, "text": { "elements": [{"text_run": {"content": "Paragraph 1"}}] } },
{ "block_type": 22, "divider": {} },
{ "block_type": 4, "heading2": { "elements": [{"text_run": {"content": "Section 2"}}] } }
],
"index": 0
}
Solution B: Serial Writes with Index
For long content requiring multiple requests, execute serially with explicit index:
Request 1: index=0, write block A
Request 2: index=1, write block B (wait for A to succeed)
Request 3: index=2, write block C (wait for B to succeed)
Solution C: Collect-Then-Write (Recommended)
LLM outputs complete Markdown -> Conversion layer -> Single API batch write
Never let the LLM write one paragraph at a time with concurrent API calls.
Complete Write Flow
1. Create document: POST /open-apis/docx/v1/documents with { "folder_token": " -> returns document_id
2. Build Block array: Convert full content to Block JSON
3. Batch write: POST .../documents/{doc_id}/blocks/{doc_id}/children?document_revision_id=-1 with all blocks
4. Container blocks (optional): For Callout etc., get block_id from step 3 response, then add children
Custom Callout Syntax
Since Markdown has no Callout equivalent, use this custom markup:
:::callout{color=yellow emoji=bulb}
Highlight content here.
Supports bold, *italic*, and lists.
:::
| Param | Values | Default | Purpose |
|-------|--------|---------|---------|
| color | red, orange, yellow, green, blue, purple, grey | yellow | Background & border |
| emoji | Any Feishu emoji_id (bulb, star, warning, fire) | bulb | Left icon |
| border | Same as color values | Same as color | Border color (override) |
Common templates:
:::callout{color=yellow emoji=bulb}
Key Insight: The most important takeaway
::::::callout{color=red emoji=warning}
Warning: Common misconception
:::
:::callout{color=green emoji=check}
Action Item: What to do next
:::
Rate Limits & Constraints
(latest version)Authentication
curl -X POST 'https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal' \
-H 'Content-Type: application/json' \
-d '{ "app_id": "", "app_secret": "" }'