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=-1
Headers:
Content-Type: application/json
Authorization: Bearer <tenant_access_token>
Body:
{
"children": [ ...Block array... ],
"index": 0
}
- •
block_id: Parent block ID (usuallydocument_iditself 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=<image_block_id>
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](url) | -- | 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
- •Create document:
POST /open-apis/docx/v1/documentswith{ "folder_token": "<token>", "title": "Title" }-> returnsdocument_id - •Build Block array: Convert full content to Block JSON
- •Batch write:
POST .../documents/{doc_id}/blocks/{doc_id}/children?document_revision_id=-1with all blocks - •Container blocks (optional): For Callout etc., get
block_idfrom 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
- •Max blocks per batch: ~50 recommended
- •Long articles: Split by H2/H3 sections, 200-500ms between batches
- •Always use
document_revision_id=-1(latest version) - •Token validity: ~2 hours, cache and refresh before expiry
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_id>", "app_secret": "<app_secret>" }'
Schema Pitfalls (Battle-tested)
- •No Markdown tables in write ops — use bullet lists instead (prevents schema errors)
- •No nested code blocks inside lists — Feishu schema validation is strict on nesting depth
- •Callout is a container — always requires a two-step create (container first, then children)
- •Each list item = separate Block — don't try to put multiple items in one block
References
- •Create Blocks API: https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/document-docx/docx-v1/document-block-children/create
- •Block Data Structure: https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/document-docx/docx-v1/data-structure/block
- •Convert API: https://open.feishu.cn/document/ukTMukTMukTM/uUDN04SN0QjL1QDN/document-docx/docx-v1/document/convert
- •Extended API reference: See
FEISHU_API_HANDBOOK.mdin workspace root