Feishu Block Ops
Low-level Feishu document block operations via REST API. Use when feishu_doc built-in actions are insufficient: batch update cells, precise position insert,...
Description
name: feishu-block-ops description: | Low-level Feishu document block operations via REST API. Use when feishu_doc built-in actions are insufficient: batch update cells, precise position insert, traverse block tree, table row/column manipulation, image replacement, or any operation requiring direct block-level control. Complements feishu-doc, not a replacement.
Feishu Block Operations
Direct REST API operations for Feishu cloud documents when the feishu_doc tool's built-in actions don't cover your needs.
When to Use This (vs feishu_doc)
| Need | Use |
|---|---|
| Read/write/append document | feishu_doc |
| Create simple table | feishu_doc create_table_with_values |
| Upload image/file | feishu_doc upload_image/upload_file |
| Batch update 200 cells at once | This skill |
| Insert content at exact position | This skill (or feishu-md2blocks) |
| Traverse block tree | This skill |
| Table row/column insert/delete | This skill |
| Merge/unmerge table cells | This skill |
| Replace images in-place | This skill |
| Delete blocks by index range | This skill |
Authentication
Get tenant access token from OpenClaw config:
import json, urllib.request
def get_feishu_token():
with open(os.path.expanduser("~/.openclaw/openclaw.json")) as f:
c = json.load(f)["channels"]["feishu"]
payload = json.dumps({"app_id": c["appId"], "app_secret": c["appSecret"]}).encode()
req = urllib.request.Request(
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
data=payload, headers={"Content-Type": "application/json"}, method="POST")
return json.loads(urllib.request.urlopen(req).read())["tenant_access_token"]
All API calls use header: Authorization: Bearer {token}
Rate Limits
| Operation | Limit |
|---|---|
| Read (GET) | 5 req/sec per app |
| Write (POST/PATCH/DELETE) | 3 req/sec per app, 3 req/sec per document |
Use time.sleep(0.35) between write calls. For reads, time.sleep(0.25).
API Reference
Base URL: https://open.feishu.cn/open-apis/docx/v1/documents
1. Get Block
GET /docx/v1/documents/{doc}/blocks/{block_id}
Returns single block with full content (type, elements, children IDs, styles).
2. Get Children (with optional full tree)
GET /docx/v1/documents/{doc}/blocks/{block_id}/children
?with_descendants=true # get ALL descendants, not just direct children
&page_size=500 # max 500
&document_revision_id=-1 # latest revision
Tip: Use with_descendants=true on table blocks to get all cells + cell content in one call.
3. Create Blocks (simple, flat only)
POST /docx/v1/documents/{doc}/blocks/{parent_id}/children
Body: {"children": [...blocks], "index": 0}
- Max 50 blocks per call
- Cannot create nested structures (e.g. table with cell content)
indexin body: 0=beginning, -1=end (default)
4. Create Nested Blocks (tables, grids, etc.)
POST /docx/v1/documents/{doc}/blocks/{parent_id}/descendant
Body: {
"children_id": ["temp_id_1", "temp_id_2"],
"descendants": [...all_blocks_with_parent_child_relations],
"index": 0
}
- Max 1000 blocks per call
children_id: only first-level child IDs (NOT grandchildren — causes error 1770006)descendants: flat array of ALL blocks including nested ones, each withblock_id,block_type,children(list of child temp IDs)- ⚠️
indexMUST be in request body, NOT as URL query parameter —?index=Nis silently ignored
5. Batch Update Blocks
PATCH /docx/v1/documents/{doc}/blocks/batch_update
Body: {"requests": [...update_requests]}
Max 200 blocks per call. Each request object contains block_id + one operation:
| Operation | Purpose |
|---|---|
update_text_elements |
Replace text content + inline elements |
update_text_style |
Change alignment, folded, language, wrap, background_color |
update_table_property |
Modify column widths, header rows/columns |
insert_table_row |
Insert rows at index |
insert_table_column |
Insert columns at index |
delete_table_rows |
Delete rows by index + count |
delete_table_columns |
Delete columns by index + count |
merge_table_cells |
Merge cells (row_start, row_end, column_start, column_end) |
unmerge_table_cells |
Unmerge previously merged cells |
replace_image |
Replace image block's content with new file_token |
Example: batch update text in multiple cells
requests = []
for block_id, new_text in updates.items():
requests.append({
"block_id": block_id,
"update_text_elements": {
"elements": [{"text_run": {"content": new_text}}]
}
})
api_call(token, "PATCH",
f"https://open.feishu.cn/open-apis/docx/v1/documents/{doc}/blocks/batch_update",
{"requests": requests})
6. Update Single Block
PATCH /docx/v1/documents/{doc}/blocks/{block_id}
Body: {same operations as batch_update, without block_id wrapper}
7. Delete Blocks
DELETE /docx/v1/documents/{doc}/blocks/{parent_id}/children/batch_delete
Body: {"start_index": 0, "end_index": 5}
- ⚠️ Uses
start_index/end_index(half-open interval[start, end)), NOTblock_ids - Indices are relative to the parent block's children list
Block Types
| Type | ID | Notes |
|---|---|---|
| Page | 1 | Document root, always one |
| Text | 2 | Plain paragraph |
| Heading1–9 | 3–11 | |
| Bullet | 12 | Unordered list item |
| Ordered | 13 | Ordered list item |
| Code | 14 | Code block |
| Quote | 15 | Block quote |
| Todo | 17 | Checkbox item |
| Callout | 19 | Highlighted block |
| Divider | 22 | Horizontal rule (body: {}) |
| Grid | 24 | Multi-column layout |
| GridColumn | 25 | Column in grid |
| Image | 27 | Image block |
| Table | 31 | Table container |
| TableCell | 32 | Cell in table |
| QuoteContainer | 34 | Quote wrapper (body: {}) |
Text Elements
Text blocks contain an elements array. Each element is one of:
# Plain text
{"text_run": {"content": "hello", "text_element_style": {"bold": True, "link": {"url": "..."}}}}
# Mention user
{"mention_user": {"user_id": "ou_xxx", "text_element_style": {}}}
# Mention document
{"mention_doc": {"token": "xxx", "obj_type": 22, "text_element_style": {}}}
# Equation (LaTeX)
{"equation": {"content": "E=mc^2"}}
# Reminder
{"reminder": {"expire_time": 1234567890, "is_whole_day": True}}
Common Patterns
Pattern 1: Read table content
# 1. Get table's descendants in one call
url = f".../blocks/{table_block_id}/children?with_descendants=true&page_size=500&document_revision_id=-1"
items = api_call(token, "GET", url)["data"]["items"]
# 2. Extract text from cells
for item in items:
if item["block_type"] == 2 and "text" in item:
text = "".join(e.get("text_run", {}).get("content", "") for e in item["text"]["elements"])
Pattern 2: Insert Markdown at position
Use feishu-md2blocks skill's md2blocks.py script with --after <block_id>.
Or manually:
# 1. Convert markdown
convert_resp = api_call(token, "POST", ".../blocks/convert",
{"content_type": "markdown", "content": md_text})
# 2. Clean table blocks (remove merge_info)
for block in convert_resp["data"]["blocks"]:
if block.get("block_type") == 31 and "table" in block:
block["table"]["property"].pop("merge_info", None)
# 3. Insert at position (index IN BODY)
api_call(token, "POST", f".../blocks/{parent_id}/descendant", {
"children_id": convert_resp["data"]["first_level_block_ids"],
"descendants": convert_resp["data"]["blocks"],
"index": target_index
})
Pattern 3: Batch edit table cells
# 1. Get all descendants of table
items = get_descendants(table_id)
# 2. Build update map
updates = []
for item in items:
if needs_update(item):
updates.append({
"block_id": item["block_id"],
"update_text_elements": {
"elements": [{"text_run": {"content": new_value}}]
}
})
# 3. Batch update (max 200 per call)
for i in range(0, len(updates), 200):
api_call(token, "PATCH", f".../blocks/batch_update",
{"requests": updates[i:i+200]})
time.sleep(0.35)
Pattern 4: Delete then re-insert (position workaround)
When you need to replace content at a specific position:
# 1. Find the index range to replace
children = get_doc_children(doc)
start_idx = children.index(first_block_to_replace)
end_idx = children.index(last_block_to_replace) + 1
# 2. Delete old blocks
api_call(token, "DELETE", f".../children/batch_delete",
{"start_index": start_idx, "end_index": end_idx})
# 3. Insert new content at same position
api_call(token, "POST", f".../blocks/{doc}/descendant", {
"children_id": new_ids,
"descendants": new_blocks,
"index": start_idx
})
Gotchas & Lessons Learned
/descendantindex in body, not URL — The most common pitfall.?index=Ncompiles but is silently ignored.batch_deleteuses index range —{"start_index": 0, "end_index": 5}deletes children[0..4]. Do NOT passblock_ids.- Table
merge_infois read-only — Must strip from blocks before insertion or API returns error. children_idis first-level only — Including grandchild IDs inchildren_idcauses error 1770006.- Rate limit is per-document — Multiple concurrent edits to the same doc share the 3/sec limit.
with_descendants=truesaves calls — One GET instead of N+1 for reading table content.- Convert API returns temp IDs — After insertion, actual block IDs differ from the temp IDs used during convert.
Reviews (0)
No reviews yet. Be the first to review!
Comments (0)
No comments yet. Be the first to share your thoughts!