A standardized process for encapsulating a DingTalk API domain into a reusable skill.Each step must actually be completed and verified before proceeding to the next step; no skipping is allowed.
Process Overview
1. [探索] SDK 模块探索 → 整理接口清单与字段结构
2. [SDK] Python SDK 测试 → 全部通过才能继续
3. [HTTP] 纯 requests HTTP 测试 → 全部通过才能继续
4. [创作] 编写 SKILL.md + references/api.md
5. [收尾] 更新 README.md / README_EN.md / AGENTS.md
Key Principles: All tests use Python (no .sh scripts).
Phase 2 and Phase 3'suv run pytestmust actually run and all be green before proceeding to Phase 4.
New API Priority + Token Incompatibility Principle: The DingTalk platform has two API systems, old and new. The new version must be used preferentially, and the two types of token must not be mixed:
- New version (recommended):
POST https://api.dingtalk.com/v1.0/oauth2/accessToken→ returnsaccessToken+expireIn, paired with theapi.dingtalk.cominterface, placed in the Headerx-acs-dingtalk-access-token- Old version (avoid):
GET https://oapi.dingtalk.com/gettoken→ returnsaccess_token, paired with theoapi.dingtalk.cominterface, placed in URL parameters?access_token=- Mutually incompatible: Using a new-version token for an old-version interface, or an old-version token for a new-version interface, will both result in 401/403, and the error message will not state that it is a token type error, making troubleshooting difficult
- Sole exception: userId → unionId conversion (
oapi.dingtalk.com/topapi/v2/user/get) currently has no v1.0 equivalent interface, so the old-version token is still required; this is a known exception, and OLD_TOKEN is used only in this conversion step and must not be passed to other API calls dt_helper.sh: Each skill'sscripts/dt_helper.shencapsulates basic capabilities such as token acquisition and caching, userId↔unionId conversion, and configuration read/write. Inbash scripts(such as execution scripts), it can be called directly;Python tests (Phase 2/3) still use inline requests logic and do not depend on dt_helper.sh.
Phase 1: SDK Exploration
1.1 Find the SDK module
# 列出与目标领域相关的 SDK 模块
ls /home/breath/project/personal/dingtalk-skills/tests/.venv/lib/python3.13/site-packages/alibabacloud_dingtalk/ \
| grep -i <关键词>
# 示例:todo / attend / approval / calendar
1.2 Extract the interface list
SDK_BASE=tests/.venv/lib/python3.13/site-packages/alibabacloud_dingtalk/<module>
# 列出所有同步方法(去掉 async)
grep -n "def " $SDK_BASE/client.py | grep -v async
# 提取 HTTP endpoint 路径 + 方法
grep -A2 "pathname=" $SDK_BASE/client.py | grep -E "pathname=|method=" | paste - -
1.3 Understand request/response fields
# 查看核心 Request 类
grep -n "^class " $SDK_BASE/models.py
# 读取关键请求类的 __init__ 和 to_map
sed -n '<start>,<end>p' $SDK_BASE/models.py
1.4 Actually probe the real fields (important!)
SDK documentation is not necessarily accurate;you must use real requests to probe the field names actually returned.
⚠️ The probing phase must use the SDK; it is forbidden to use
requeststo send HTTP requests directly.
The purpose of probing is to learn the SDK's actual behavior and field structure. Only by using the SDK can you simultaneously verify interface callability and field names.
requestsRaw requests are left for Phase 3 (pure HTTP testing) and must not be introduced early in the probing phase.
Get token and unionId (using dt_helper.sh):
# 在 skill 根目录或 tests/ 目录下
TOKEN=$(bash scripts/dt_helper.sh --token) # 新版 token,带缓存
UNION_ID=$(bash scripts/dt_helper.sh --to-unionid) # 自动从 DINGTALK_MY_USER_ID 转换
Probe script (called using the SDK):
# 在 tests/ 目录下运行(已有 .env 和 venv)
import os, pathlib, subprocess
from alibabacloud_dingtalk.<module>_1_0 import client as dt_client, models as dt_models
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_tea_util import models as util_models
# 读取 .env
env = pathlib.Path(".env").read_text()
for line in env.splitlines():
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, _, v = line.partition("="); os.environ.setdefault(k.strip(), v.strip())
# 获取 token(调 dt_helper.sh)
token = subprocess.check_output(
["bash", "../scripts/common/dt_helper.sh", "--token"], text=True).strip()
union_id = os.environ["DINGTALK_MY_OPERATOR_ID"]
# SDK 客户端
cfg = open_api_models.Config(protocol="HTTPS", endpoint="api.dingtalk.com")
client = dt_client.Client(cfg)
runtime = util_models.RuntimeOptions()
# CREATE 探测 → 打印实际响应字段
h = dt_models.CreateXxxHeaders()
h.x_acs_dingtalk_access_token = token
req = dt_models.CreateXxxRequest(subject="[probe] 探测")
resp = client.create_xxx_with_options(union_id, req, h, runtime)
print("CREATE body:", vars(resp.body))
# 根据 print 输出确定实际字段名
After organizing into an interface list table, continue.
1.5 Probe and enable the required permissions (important!)
Recommended method (faster than checking documentation): Directly use the SDK to send a "dry run" request to each target interface, and from the 403 response body's requiredScopes field, collect the required permission points in bulk. This is more accurate than checking the documentation one by one.
# tests/probe_<skill>_perms.py(写到 tests/ 目录下,不要写到 /tmp/)
import os, pathlib, subprocess, re, time
from alibabacloud_dingtalk.<module>_1_0 import client as dt_client, models as dt_models
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_tea_util import models as util_models
env_path = pathlib.Path(__file__).parent / ".env"
for line in env_path.read_text().splitlines():
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, _, v = line.partition("="); os.environ.setdefault(k.strip(), v.strip())
token = subprocess.check_output(
["bash", str(pathlib.Path(__file__).parent.parent / "scripts/common/dt_helper.sh"), "--token"],
text=True).strip()
user_id = os.environ["DINGTALK_MY_USER_ID"]
cfg = open_api_models.Config(protocol="HTTPS", endpoint="api.dingtalk.com")
client = dt_client.Client(cfg)
runtime = util_models.RuntimeOptions()
def h(cls):
obj = cls(); obj.x_acs_dingtalk_access_token = token; return obj
cases = [
("接口名1", lambda: client.create_xxx_with_options(..., h(dt_models.CreateXxxHeaders), runtime)),
("接口名2", lambda: client.get_xxx_with_options(..., h(dt_models.GetXxxHeaders), runtime)),
]
for name, fn in cases:
try:
fn(); print(f"[OK] {name}")
except Exception as e:
msg = str(e)
scopes = re.findall(r"'[\w.]+(?:Read|Write)'", msg)
if "403" in msg:
print(f"[403] {name} → 需要权限: {', '.join(scopes) or msg[:120]}")
else:
print(f"[ERR] {name} → {msg[:120]}")
After collecting all permission points, go to "Developer Console → App Permission Management" to enable them. After publishing, rerun to confirm OK, then continue to Phase 2.
Write the final permission list into the "Required App Permissions" section of references/api.md .
When probing, pass in an "obviously invalid" parameter (such as
process_code="PROC-000000"). The purpose is only to trigger a 403 and obtainrequiredScopes; it does not matter whether the business logic is correct.
Phase 2: Python SDK Testing (must be all green to continue)
2.1 Directory Structure
tests/
├── .env
├── conftest.py # 全局 fixture(token, operator_id, api_headers)
└── dingtalk-<skill-name>/
├── __init__.py
├── conftest.py # 技能专属 fixture(union_id、共享测试资源)
├── test_<module>_sdk.py # 阶段二:SDK 测试
└── test_<module>.py # 阶段三:纯 HTTP 测试
2.2 SDK Client Configuration (applicable to all DingTalk v1.0 APIs)
from alibabacloud_dingtalk.<module>_1_0 import client as dt_client, models as dt_models
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_tea_util import models as util_models
@pytest.fixture(scope="session")
def sdk_client():
config = open_api_models.Config()
config.protocol = "HTTPS" # 必须是 HTTPS,HTTP 会 404
config.endpoint = "api.dingtalk.com"
return dt_client.Client(config)
@pytest.fixture(scope="session")
def sdk_runtime():
return util_models.RuntimeOptions()
2.3 SDK Test Template
def test_sdk_create(sdk_client, sdk_runtime, token, union_id):
h = dt_models.CreateXxxHeaders()
h.x_acs_dingtalk_access_token = token
req = dt_models.CreateXxxRequest(subject="[sdk] 测试", operator_id=union_id)
resp = sdk_client.create_xxx_with_options(union_id, req, h, sdk_runtime)
item = resp.body
assert item.id, "响应无 id" # 字段名先探测,再写断言
# 清理
dh = dt_models.DeleteXxxHeaders(); dh.x_acs_dingtalk_access_token = token
dr = dt_models.DeleteXxxRequest(operator_id=union_id)
sdk_client.delete_xxx_with_options(union_id, item.id, dr, dh, sdk_runtime)
2.4 Run and Confirm All Green
cd tests
uv run pytest dingtalk-<skill-name>/test_<module>_sdk.py -v
# ✅ 全部 PASSED(允许有权限缺失导致的 SKIPPED)才能继续
Phase 3: Pure HTTP Request Testing (must be all green to continue)
3.1 Description
Do not use the SDK at all; use only the requests library to call the REST API directly, verifying the HTTP behavior of the interface.
This is the second layer of verification independent of the SDK, ensuring that the endpoint path, request fields, HTTP method, and response fields are all correct.
3.2 Test Template
"""
纯 HTTP 测试:不依赖 SDK,验证接口 HTTP 行为
响应字段(从实际探测确认):
- 创建: id, subject, done, priority
- 更新: result=True
- 删除: result=True,删后 GET → 400
"""
import requests
BASE = "https://api.dingtalk.com/v1.0/xxx/users"
def _create(uid, headers, subject, **kwargs):
r = requests.post(f"{BASE}/{uid}/items", params={"operatorId": uid},
headers=headers, json={"subject": subject, **kwargs}, timeout=15)
assert r.status_code == 200, f"创建失败:{r.text}"
d = r.json()
assert "id" in d, f"缺少 id:{d}" # 用探测到的实际字段名
return d
def _delete(uid, headers, item_id):
r = requests.delete(f"{BASE}/{uid}/items/{item_id}", params={"operatorId": uid},
headers=headers, timeout=15)
assert r.status_code == 200, f"删除失败:{r.text}"
return r.json()
def test_http_create_basic(union_id, api_headers):
d = _create(union_id, api_headers, "[http] 基础创建")
assert d["id"]
_delete(union_id, api_headers, d["id"])
# 权限不足的接口 → 遇到 403 时 pytest.skip
def test_http_list(union_id, api_headers):
r = requests.post(f"{BASE}/{union_id}/items/list", params={"operatorId": union_id},
headers=api_headers, json={"isDone": False}, timeout=15)
if r.status_code == 403:
pytest.skip(f"缺少权限 → {r.json().get('message','')[:100]}")
assert r.status_code == 200
assert "items" in r.json() # 使用实际响应字段名
3.3 Run and Confirm All Green
uv run pytest dingtalk-<skill-name>/test_<module>.py -v
# ✅ 全部 PASSED/SKIPPED 才能继续(FAILED = 禁止进入阶段四)
Phase 4: Write SKILL.md + references/api.md
Execute this phase only after both Phase 2 and Phase 3 have fully passed.
4.1 Directory Structure
.agents/skills/dingtalk-<skill-name>/
├── SKILL.md
└── references/
└── api.md
4.2 SKILL.md Template
Use the SKILL.md of dingtalk-message or dingtalk-todo as a template; it must include:
- frontmatter:
name+description(trigger keywords must be comprehensive) - Workflow: read configuration → collect missing items → persist → obtain Token → execute operations
- Configuration Item Table: a description of the source of each
CONFIGkey. Prioritize collectingDINGTALK_MY_USER_ID(enterprise employee ID, which can be found in the admin console contacts; it is not a mobile phone number and not a unionId). If the API requires a unionId, the script automatically converts it - Identity Identifier Description: the difference between userId vs unionId, explaining which ID the skill's API uses, and the automatic conversion logic
- Execution Script Template: a complete bash script (
create_file /tmp/<task>.shthenbashto execute; heredoc is prohibited), callingscripts/dt_helper.shto obtain the token and unionId, with no need for inline token caching or id conversion logic:HELPER="<THE_SKILLMD_FILE_PATH>/scripts/dt_helper.sh" TOKEN=$(bash "$HELPER" --token) # 新版 token(带缓存) UNION_ID=$(bash "$HELPER" --to-unionid) # 自动转换,如 API 需要 userId 则用 --get DINGTALK_MY_USER_ID USER_ID=$(bash "$HELPER" --get DINGTALK_MY_USER_ID) - Detailed Reference: points to
references/api.md; use grep to look up index patterns
Note: Different APIs use different user ID types. For example, the messaging API only accepts userId, while the todo API only accepts unionId. This must be clearly explained when writing SKILL.md.
Skill Independence Principle: each skill iscompletely independent—when an agent loads a skill, it can only see the SKILL.md of that one skill,and is completely unaware of the existence and content of other skills. Therefore:
- The identity conversion logic (userId → unionId) must be fully written into each SKILL.md of each skill that needs it; it cannot reference other skills
- There are only two cross-skill sharing mechanisms:
~/.dingtalk-skills/config(configuration file) andscripts/dt_helper.sh(tool script, distributed bycommon_scripts_load.shto each skill'sscripts/directory)- The bash execution script directly calls
scripts/dt_helper.sh, with no need for inline token caching or userId→unionId conversion logic- The explanatory text in SKILL.md (workflow, configuration item table, identity description, etc.) must still be complete and self-contained—the agent will only read the SKILL.md of the current skill
4.3 references/api.md Specification
# 钉钉 <功能> API 参考
每个接口包含:
- 完整请求体 JSON 示例(含必填标注)
- 实际响应 JSON 示例(从测试中截取)
- 错误响应示例(含状态码)
- curl 示例
## 错误码表
| HTTP 状态 | 错误码 | 说明 | 处理建议 |
Phase Five: Final Updates
# 1. AGENTS.md 技能目录新增一行
# 2. README.md 技能列表新增一行(含安装命令和能力表)
# 3. README_EN.md 技能列表新增对应英文条目
# 4. tests/.env.example 补充新技能所需配置项(如有)