Skip to content
Back to skills

Feishu Block Ops

ASecurity

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.

  • 14 stars
  • 0 votes
  • 0 copies
  • 8 views
  • Added September 7, 2026
securitypythongoapi

Works with

  • api

Security analysis

A100/100

Scanned September 7, 2026

npx -y skills add modbender/skill-library-mcp --skill feishu-block-ops --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Feishu Block Ops?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Feishu Block Ops
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/modbender-feishu-block-ops/badge)](https://www.skillsdirectory.com/skills/modbender-feishu-block-ops)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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:

```python
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)
- `index` in 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 with `block_id`, `block_type`, `children` (list of child temp IDs)
- ⚠️ **`index` MUST be in request body, NOT as URL query parameter** — `?index=N` is 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

```python
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)`), **NOT** `block_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:

```python
# 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

```python
# 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:
```python
# 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

```python
# 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:

```python
# 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

1. **`/descendant` index in body, not URL** — The most common pitfall. `?index=N` compiles but is silently ignored.
2. **`batch_delete` uses index range** — `{"start_index": 0, "end_index": 5}` deletes children[0..4]. Do NOT pass `block_ids`.
3. **Table `merge_info` is read-only** — Must strip from blocks before insertion or API returns error.
4. **`children_id` is first-level only** — Including grandchild IDs in `children_id` causes error 1770006.
5. **Rate limit is per-document** — Multiple concurrent edits to the same doc share the 3/sec limit.
6. **`with_descendants=true` saves calls** — One GET instead of N+1 for reading table content.
7. **Convert API returns temp IDs** — After insertion, actual block IDs differ from the temp IDs used during convert.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…