dingtalk-ai-table

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

钉钉 AI 表格技能

DingTalk AI Table Skill

负责钉钉 AI 表格(
.able
格式多维表格)的所有操作,通过钉钉开放平台 Notable API 实现。
核心概念:
  • AI 表格
    .able
    文件):多维表格,使用 Notable API(
    /v1.0/notable
    ),不是普通电子表格
  • base_id:AI 表格文件的 nodeId,是表格在钉钉文档系统中的唯一标识
  • 工作表(Sheet):AI 表格内的单张表,包含字段和记录
  • 字段(Field):列定义,有名称和类型(
    text
    number
    date
    等)
  • 记录(Record):数据行,包含各字段的值
API 详情见
references/api.md

Responsible for all operations of DingTalk AI Table (multi-dimensional table in
.able
format), implemented via the Notable API of DingTalk Open Platform.
Core Concepts:
  • AI Table (
    .able
    file): Multi-dimensional table, uses Notable API (
    /v1.0/notable
    ), is not a regular spreadsheet
  • base_id: nodeId of the AI Table file, the unique identifier of the table in DingTalk document system
  • Worksheet (Sheet): A single table inside the AI Table, contains fields and records
  • Field: Column definition, with name and type (
    text
    ,
    number
    ,
    date
    , etc.)
  • Record: Data row, contains values of each field
For API details, see
references/api.md
.

配置管理(每次开始前必读)

Configuration Management (Must Read Before Each Start)

配置文件路径

Configuration File Path

~/.dingtalk-skills/config
(跨会话保留,所有 dingtalk-skills 共用同一文件)
~/.dingtalk-skills/config
(persisted across sessions, shared by all dingtalk-skills)

本技能需要的配置说明

Configuration Instructions Required for This Skill

说明来源
DINGTALK_APP_KEY
钉钉应用 appKey开放平台 → 应用管理 → 凭证信息
DINGTALK_APP_SECRET
钉钉应用 appSecret同上
DINGTALK_OPERATOR_ID
操作人 unionId见下方"为什么需要 operatorId"章节
DINGTALK_AI_TABLE_BASE_ID
AI 表格的 nodeId从 AI 表格分享链接提取
KeyDescriptionSource
DINGTALK_APP_KEY
DingTalk application appKeyOpen Platform → Application Management → Credential Information
DINGTALK_APP_SECRET
DingTalk application appSecretSame as above
DINGTALK_OPERATOR_ID
Operator unionIdSee the "Why operatorId is required" section below
DINGTALK_AI_TABLE_BASE_ID
nodeId of AI TableExtracted from AI Table share link

启动流程(每次执行任务前)

Startup Process (Before Each Task Execution)

  1. 读取配置:检查
    ~/.dingtalk-skills/config
    是否存在,解析已有键值
  2. 识别缺失项:找出上表中尚未配置的键
  3. 一次性收集:将所有缺失项合并为一条提问,不要逐条询问,例如:
    需要以下信息才能继续(已有的无需再填):
    • 钉钉应用 appKey(钉钉开放平台 → 应用管理 → 凭证信息)
    • 钉钉应用 appSecret
    • AI 表格链接(用于提取 base_id)
    • 你的钉钉 userId 或 unionId(以便以你的身份操作表格)
  4. 持久化:将用户提供的值追加写入 config,后续直接读取,无需再问
  5. 执行任务:配置完整后开始操作
注意
APP_KEY
/
APP_SECRET
/
OPERATOR_ID
属于凭证,禁止在输出中完整打印,确认时仅显示前 4 位 +
****

  1. Read configuration: Check if
    ~/.dingtalk-skills/config
    exists, parse existing key-value pairs
  2. Identify missing items: Find the keys in the above table that have not been configured yet
  3. One-time collection: Combine all missing items into one question, do not ask one by one, for example:
    The following information is required to proceed (no need to fill in existing information):
    • DingTalk application appKey (DingTalk Open Platform → Application Management → Credential Information)
    • DingTalk application appSecret
    • AI Table link (used to extract base_id)
    • Your DingTalk userId or unionId (to operate the table as your identity)
  4. Persistence: Append the values provided by the user to the config, read directly later without asking again
  5. Execute task: Start operation after configuration is complete
Note:
APP_KEY
/
APP_SECRET
/
OPERATOR_ID
are credentials, it is forbidden to print them completely in the output, only show the first 4 digits +
****
when confirming.

认证

Authentication

每次调用 API 前,用 appKey/appSecret 获取当次的 accessToken(有效期 2 小时):
POST https://api.dingtalk.com/v1.0/oauth2/accessToken
Content-Type: application/json

{ "appKey": "<应用 appKey>", "appSecret": "<应用 appSecret>" }
所有请求需携带:
  • 请求头:
    x-acs-dingtalk-access-token: <accessToken>
  • 查询参数:
    operatorId=<用户 unionId>
    (所有写操作及部分读操作必须)

Before each API call, use appKey/appSecret to get the current accessToken (valid for 2 hours):
POST https://api.dingtalk.com/v1.0/oauth2/accessToken
Content-Type: application/json

{ "appKey": "<应用 appKey>", "appSecret": "<应用 appSecret>" }
All requests need to carry:
  • Request header:
    x-acs-dingtalk-access-token: <accessToken>
  • Query parameter:
    operatorId=<user unionId>
    (required for all write operations and some read operations)

为什么需要 base_id

Why base_id is required

钉钉文档系统中每个文件(文档、表格、AI 表格)都有一个全局唯一的 nodeId,即
base_id
。它的作用类似数据库主键——API 通过它定位到具体是哪个 AI 表格文件,因为账号下可能有多个
.able
文件。
从链接提取 base_id:
https://alidocs.dingtalk.com/i/nodes/<base_id>?...
                                     ↑ 这一段就是 base_id
请用户提供 AI 表格的分享链接,从
/nodes/
后截取 ID 片段。首次获取后写入 config,后续无需再问。

Each file (document, spreadsheet, AI Table) in the DingTalk document system has a globally unique nodeId, which is
base_id
. Its function is similar to a database primary key - the API locates the specific AI Table file through it, because there may be multiple
.able
files under the account.
Extract base_id from link:
https://alidocs.dingtalk.com/i/nodes/<base_id>?...
                                     ↑ This part is base_id
Ask the user to provide the share link of the AI Table, and intercept the ID fragment after
/nodes/
. Write it to the config after the first acquisition, no need to ask again later.

为什么需要 operatorId(unionId)

Why operatorId (unionId) is required

钉钉开放平台要求所有写操作必须代表一个真实用户身份执行,而不是以匿名应用身份操作。
operatorId
就是声明"这个操作是谁做的"——它会被记录到变更日志、触发对应用户的通知,并用于权限校验。
  • 值为操作人的
    unionId
    (钉钉跨组织唯一 ID),不是
    userId
    (仅组织内唯一)
  • 错误传入 userId 会导致权限报错或操作归属错误
DingTalk Open Platform requires all write operations to be performed on behalf of a real user identity, not an anonymous application identity.
operatorId
is to declare "who performed this operation" - it will be recorded in the change log, trigger the corresponding user's notification, and be used for permission verification.
  • The value is the operator's
    unionId
    (DingTalk cross-organization unique ID), not
    userId
    (only unique within the organization)
  • Incorrectly passing userId will cause permission errors or incorrect operation attribution

如何获取 unionId

How to get unionId

方法一:已知 userId → 换取 unionId(最常用)
userId
通常可从通讯录、免登、消息回调等场景直接获得。
第一步:获取旧式
access_token
(与新版 accessToken 不同,此处单独获取):
GET https://oapi.dingtalk.com/gettoken?appkey=<appKey>&appsecret=<appSecret>
返回:
{ "access_token": "xxx", "expires_in": 7200 }
第二步:用 userId 查询用户详情,取出 unionId:
POST https://oapi.dingtalk.com/topapi/v2/user/get?access_token=<旧式token>
Content-Type: application/json

{ "userid": "<钉钉 userId>" }
返回字段
result.unionid
(无下划线)即为 unionId。
注意:
result.union_id
(有下划线)在专属钉钉组织中可能为空,请使用
result.unionid
方法二:机器人/消息场景
用户通过钉钉机器人或消息触发时,消息体中直接包含
senderUnionId
字段,可直接作为
operatorId
使用,无需额外查询。
方法三:直接询问用户
若上述方式不可用,在初始配置阶段询问用户提供 userId 或 unionId(均可在钉钉个人信息页查看)。获取后写入 config,后续操作无需重复获取。

Method 1: Known userId → exchange for unionId (most commonly used)
userId
can usually be obtained directly from scenarios such as address book, free login, message callback, etc.
Step 1: Get the old
access_token
(different from the new version of accessToken, obtained separately here):
GET https://oapi.dingtalk.com/gettoken?appkey=<appKey>&appsecret=<appSecret>
Return:
{ "access_token": "xxx", "expires_in": 7200 }
Step 2: Query user details with userId and get unionId:
POST https://oapi.dingtalk.com/topapi/v2/user/get?access_token=<旧式token>
Content-Type: application/json

{ "userid": "<钉钉 userId>" }
The returned field
result.unionid
(no underscore) is unionId.
Note:
result.union_id
(with underscore) may be empty in exclusive DingTalk organizations, please use
result.unionid
.
Method 2: Bot/message scenario
When a user triggers via DingTalk bot or message, the message body directly contains the
senderUnionId
field, which can be used directly as
operatorId
without additional query.
Method 3: Ask the user directly
If the above methods are not available, ask the user to provide userId or unionId during the initial configuration phase (both can be viewed on the DingTalk personal information page). Write it to the config after acquisition, no need to obtain it repeatedly for subsequent operations.

核心操作

Core Operations

1. 列出工作表

1. List worksheets

GET https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets?operatorId={operatorId}
x-acs-dingtalk-access-token: <accessToken>
返回:
json
{
  "value": [
    { "id": "HAcL4SD", "name": "项目" },
    { "id": "nr2iEiW", "name": "任务" }
  ]
}

GET https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets?operatorId={operatorId}
x-acs-dingtalk-access-token: <accessToken>
Return:
json
{
  "value": [
    { "id": "HAcL4SD", "name": "项目" },
    { "id": "nr2iEiW", "name": "任务" }
  ]
}

2. 查询单个工作表

2. Query single worksheet

GET https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}?operatorId={operatorId}
返回:
{ "id": "HAcL4SD", "name": "项目" }

GET https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}?operatorId={operatorId}
Return:
{ "id": "HAcL4SD", "name": "项目" }

3. 新建工作表

3. Create new worksheet

POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets?operatorId={operatorId}
Content-Type: application/json

{
  "name": "新工作表名称",
  "fields": [
    { "name": "标题", "type": "text" },
    { "name": "数量", "type": "number" }
  ]
}
fields
可选(不传则创建空工作表)。
返回:
{ "id": "新sheetId", "name": "新工作表名称" }

POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets?operatorId={operatorId}
Content-Type: application/json

{
  "name": "新工作表名称",
  "fields": [
    { "name": "标题", "type": "text" },
    { "name": "数量", "type": "number" }
  ]
}
fields
is optional (empty worksheet is created if not passed).
Return:
{ "id": "新sheetId", "name": "新工作表名称" }

4. 删除工作表

4. Delete worksheet

DELETE https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}?operatorId={operatorId}
返回:
{ "success": true }

⚠️ 不可恢复,执行前需用户确认。

DELETE https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}?operatorId={operatorId}
Return:
{ "success": true }

⚠️ Unrecoverable, user confirmation is required before execution.

5. 列出字段

5. List fields

GET https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/fields?operatorId={operatorId}
返回:
json
{
  "value": [
    { "id": "6mNRNHb", "name": "标题", "type": "text" },
    { "id": "BDGLCo2", "name": "截止日期", "type": "date", "property": { "formatter": "YYYY-MM-DD" } },
    { "id": "mr8APlG", "name": "数量", "type": "number", "property": { "formatter": "INT" } }
  ]
}

GET https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/fields?operatorId={operatorId}
Return:
json
{
  "value": [
    { "id": "6mNRNHb", "name": "标题", "type": "text" },
    { "id": "BDGLCo2", "name": "截止日期", "type": "date", "property": { "formatter": "YYYY-MM-DD" } },
    { "id": "mr8APlG", "name": "数量", "type": "number", "property": { "formatter": "INT" } }
  ]
}

6. 新建字段

6. Create new field

POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/fields?operatorId={operatorId}
Content-Type: application/json

{
  "name": "字段名称",
  "type": "number"
}
type
常用值:
text
(文本)、
number
(数字)、
date
(日期)
返回:
{ "id": "新fieldId", "name": "字段名称", "type": "number", "property": { "formatter": "INT" } }

POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/fields?operatorId={operatorId}
Content-Type: application/json

{
  "name": "字段名称",
  "type": "number"
}
Common values for
type
:
text
(text),
number
(number),
date
(date)
Return:
{ "id": "新fieldId", "name": "字段名称", "type": "number", "property": { "formatter": "INT" } }

7. 更新字段

7. Update field

PUT https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/fields/{field_id}?operatorId={operatorId}
Content-Type: application/json

{
  "name": "新字段名称"
}
返回:
{ "id": "fieldId" }
(通过重新查询列表确认名称已变更)

PUT https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/fields/{field_id}?operatorId={operatorId}
Content-Type: application/json

{
  "name": "新字段名称"
}
Return:
{ "id": "fieldId" }
(confirm the name has been changed by re-querying the list)

8. 删除字段

8. Delete field

DELETE https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/fields/{field_id}?operatorId={operatorId}
返回:
{ "success": true }

⚠️ 删除字段会同时删除该列所有数据,执行前需用户确认。

DELETE https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/fields/{field_id}?operatorId={operatorId}
Return:
{ "success": true }

⚠️ Deleting a field will delete all data in this column at the same time, user confirmation is required before execution.

9. 新增记录(最常用操作)

9. Add records (most commonly used operation)

POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/records?operatorId={operatorId}
Content-Type: application/json

{
  "records": [
    { "fields": { "标题": "任务一", "数量": 3 } },
    { "fields": { "标题": "任务二", "数量": 5 } }
  ]
}
fields
中使用字段名称(非 ID)作为键。
返回:
{ "value": [{ "id": "记录ID1" }, { "id": "记录ID2" }] }

POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/records?operatorId={operatorId}
Content-Type: application/json

{
  "records": [
    { "fields": { "标题": "任务一", "数量": 3 } },
    { "fields": { "标题": "任务二", "数量": 5 } }
  ]
}
Use field name (not ID) as the key in
fields
.
Return:
{ "value": [{ "id": "记录ID1" }, { "id": "记录ID2" }] }

10. 查询记录列表

10. Query record list

POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/records/list?operatorId={operatorId}
Content-Type: application/json

{
  "maxResults": 20,
  "nextToken": ""
}
返回:
json
{
  "records": [
    {
      "id": "RNXU1Vm2L2",
      "fields": { "标题": "任务一", "数量": 3 },
      "createdTime": 1772723541439,
      "createdBy": { "unionId": "xxx" },
      "lastModifiedTime": 1772723541439,
      "lastModifiedBy": { "unionId": "xxx" }
    }
  ],
  "hasMore": false,
  "nextToken": ""
}
翻页:上次响应
hasMore=true
时,将
nextToken
传入下次请求。

POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/records/list?operatorId={operatorId}
Content-Type: application/json

{
  "maxResults": 20,
  "nextToken": ""
}
Return:
json
{
  "records": [
    {
      "id": "RNXU1Vm2L2",
      "fields": { "标题": "任务一", "数量": 3 },
      "createdTime": 1772723541439,
      "createdBy": { "unionId": "xxx" },
      "lastModifiedTime": 1772723541439,
      "lastModifiedBy": { "unionId": "xxx" }
    }
  ],
  "hasMore": false,
  "nextToken": ""
}
Pagination: When the last response
hasMore=true
, pass
nextToken
to the next request.

11. 更新记录

11. Update record

PUT https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/records?operatorId={operatorId}
Content-Type: application/json

{
  "records": [
    { "id": "记录ID", "fields": { "标题": "新标题" } }
  ]
}
返回:
{ "value": [{ "id": "记录ID" }] }

只传需要修改的字段,未传字段保持不变。

PUT https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/records?operatorId={operatorId}
Content-Type: application/json

{
  "records": [
    { "id": "记录ID", "fields": { "标题": "新标题" } }
  ]
}
Return:
{ "value": [{ "id": "记录ID" }] }

Only pass the fields that need to be modified, the fields not passed remain unchanged.

12. 删除记录

12. Delete record

POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/records/delete?operatorId={operatorId}
Content-Type: application/json

{
  "recordIds": ["记录ID1", "记录ID2"]
}
返回:
{ "success": true }

POST https://api.dingtalk.com/v1.0/notable/bases/{base_id}/sheets/{sheet_id}/records/delete?operatorId={operatorId}
Content-Type: application/json

{
  "recordIds": ["记录ID1", "记录ID2"]
}
Return:
{ "success": true }

典型场景

Typical Scenarios

"查看 AI 表格里有哪些工作表"

"Check what worksheets are in the AI Table"

  1. 询问 AI 表格链接,提取 base_id
  2. GET /sheets
    获取工作表列表
  3. 展示工作表名称和 ID
  1. Ask for the AI Table link, extract base_id
  2. GET /sheets
    to get the worksheet list
  3. Display worksheet names and IDs

"往 AI 表格的'任务'工作表添加几条记录"

"Add several records to the 'Task' worksheet of the AI Table"

  1. GET /sheets
    找到目标工作表的 sheet_id
  2. GET /fields
    了解现有字段名称和类型
  3. POST /records
    批量插入,fields 中用字段名称作键
  4. 告知写入成功及记录 ID
  1. GET /sheets
    to find the sheet_id of the target worksheet
  2. GET /fields
    to understand the existing field names and types
  3. POST /records
    to insert in batches, use field names as keys in fields
  4. Inform that the writing is successful and the record IDs

"查询'任务'表中所有记录"

"Query all records in the 'Task' table"

  1. GET /sheets
    找到 sheet_id
  2. POST /records/list
    翻页获取所有记录
  3. 整理为表格形式展示
  1. GET /sheets
    to find sheet_id
  2. POST /records/list
    to get all records by pagination
  3. Organize into table form for display

"删除某条记录"

"Delete a record"

  1. POST /records/list
    定位目标记录(让用户确认)
  2. POST /records/delete
    传入 recordId
  3. 告知删除成功
  1. First
    POST /records/list
    to locate the target record (let user confirm)
  2. POST /records/delete
    to pass recordId
  3. Inform that the deletion is successful

"给工作表新增一个'备注'文本字段"

"Add a 'Remark' text field to the worksheet"

  1. GET /sheets
    找到 sheet_id
  2. POST /fields
    { "name": "备注", "type": "text" }
  3. 返回新字段 ID 并告知成功

  1. GET /sheets
    to find sheet_id
  2. POST /fields
    ,
    { "name": "备注", "type": "text" }
  3. Return the new field ID and inform success

字段类型参考

Field Type Reference

type含义
text
纯文本
number
数字(含
property.formatter
INT
PERCENT
等)
date
日期(含
property.formatter
:如
YYYY-MM-DD

typeMeaning
text
Plain text
number
Number (including
property.formatter
:
INT
,
PERCENT
, etc.)
date
Date (including
property.formatter
: e.g.
YYYY-MM-DD
)

错误处理

Error Handling

HTTP 状态码 / code含义处理方式
401token 过期重新获取 accessToken
403权限不足检查应用是否开通 Notable 相关权限
invalidRequest.document.notFound
base_id 无效或无权访问确认 AI 表格 nodeId 正确且已授权
404工作表或字段不存在确认 sheet_id / field_id 正确
429触发限流等待 1 秒后重试
HTTP Status Code / codeMeaningHandling Method
401Token expiredRe-acquire accessToken
403Insufficient permissionsCheck if the application has enabled Notable related permissions
invalidRequest.document.notFound
Invalid base_id or no access permissionConfirm that the AI Table nodeId is correct and authorized
404Worksheet or field does not existConfirm that sheet_id / field_id is correct
429Rate limit triggeredRetry after waiting for 1 second