Motion API
Motion (usemotion.com) REST API 的完整客户端,覆盖任务、项目、重复任务、评论、自定义字段、用户、工作区、状态和日程等全部 27 个记录在案的端点,并提供一个 raw 逃生口用于处理任何尚未记录的内容。
何时使用此技能
当工作涉及 Motion 时使用:读取或创建任务、列出项目、在任务上发布评论、管理重复任务,或读取工作区状态、用户和日程。
凭据
一个凭据:MOTION_API_KEY。当脚本通过 RunWithCredentials 运行时,平台会将其作为环境变量注入——切勿在命令行上传递密钥,也切勿打印或回显它。
在 Motion 的 设置 → API 中获取密钥。Motion 只显示一次,请立即复制。如果调用开始返回 401,说明密钥已被吊销或重新生成,需要替换。
注意:在代理模式凭据传递中,环境变量保存的是一个非秘密占位符,实际值会被注入到对 api.usemotion.com 的请求中。新保存的凭据可能需要几分钟才能传播;立即出现 401 但稍后成功是传播延迟,而不是密钥错误。
API 遵循的约定
以下事实决定了调用是成功还是失败。
| 项目 | 值 |
|---|---|
| 基础 URL | https://api.usemotion.com |
| 认证头 | X-API-Key: <key> |
| 健康检查 | GET /v1/users/me — 返回密钥的所有者 |
| 路径前缀 | 除自定义字段外所有内容使用 /v1/,自定义字段使用 /beta/ |
| 速率限制 | 12 次/分钟(个人),最高 120 次(团队),企业版更高 |
| 描述格式 | GitHub Flavored Markdown |
| 优先级值 | ASAP、HIGH、MEDIUM、LOW(重复任务仅限 HIGH/MEDIUM) |
| 截止日期类型 | HARD、SOFT、NONE(重复任务仅限 HARD/SOFT) |
分页。 列表端点接受 cursor 参数并返回一个信封结构:
{ "meta": { "nextCursor": "…", "pageSize": 25 }, "tasks": [ … ] }
信封键是 meta,下一页字段是 nextCursor。在任何列表命令上传递 --all,脚本会自动跟踪游标,返回一个扁平数组。
--all 在100 页后停止并失败,因此失控的查询不会导致运行挂起——如果遇到此情况,请缩小查询范围。此外,如果某一页既不是 JSON 对象也不是数组(截断或网关错误页面),游标遍历会拒绝而不是返回已有内容:看起来完整的部分列表比错误更糟糕。
两个列表端点返回裸数组且没有分页——GET /v1/statuses 和 GET /beta/workspaces/{id}/custom-fields。其他所有端点都使用信封结构。
错误。 非 2xx 响应携带 {"message", "error", "statusCode"}。脚本会以 JSON 形式将其打印到 stderr 并以非零退出。429 会进行退避重试,并遵守 Retry-After 头;400、401、403 和 404 会立即显示且不会重试。验证失败会携带逐字段的 errors[] 数组;客户端会附加这些消息,因此实际原因可见。
User-Agent 是必需的。 Motion 的边缘网关会阻止默认的 Python-urllib/x.y 签名,并返回 Cloudflare 403 (browser_signature_banned),请求无法到达 API。客户端始终发送 motion-api-skill/1.0;如果你自己编写针对此 API 的请求,请显式设置 User-Agent,否则你将收到令人困惑的 403 而不是真实响应。
布尔查询参数必须是小写。 Motion 将查询字符串解析为文本,因此布尔过滤器必须是字面量 true,而不是 Python 的 True。弄错这一点不会导致静默无操作:API 会返回令人困惑的 400,并且消息会提及相反值(请参阅下面的实时差异部分)。客户端始终发送小写字面量。
命令参考
每个命令都会将 JSON 打印到 stdout。python3 motion.py <group> --help 列出所有标志。
任务
| 命令 | 端点 |
|---|---|
tasks list | GET /v1/tasks |
tasks get <task_id> | GET /v1/tasks/{id} |
tasks create | POST /v1/tasks |
tasks update <task_id> | PATCH /v1/tasks/{id} |
tasks move <task_id> --workspace-id … | PATCH /v1/tasks/{id}/move |
tasks delete <task_id> --yes | DELETE /v1/tasks/{id} |
tasks unassign <task_id> --yes | DELETE /v1/tasks/{id}/assignee |
tasks list 过滤器:--assignee-id、--project-id、--workspace-id、--name(不区分大小写的子字符串)、--label(单数,一个标签名称)、--status(可重复)、--include-all-statuses。--status 和 --include-all-statuses 是互斥的——同时发送会被 API 拒绝,CLI 会提前拒绝并显示该消息,而不是发送一个肯定会返回 400 的请求。
--include-all-statuses 是查看已移出工作区默认状态的任务(例如所有位于 Backlog 或 In Review 的任务)的方法;没有它,GET /v1/tasks 仅返回默认状态,列表看起来为空。将其与 --all 结合使用会返回分配给某人的所有任务,跨越所有状态。
tasks create / tasks update 接受相同的字段:--name、--workspace-id、--due-date(ISO 8601;计划任务必需)、--duration、--status、--project-id、--description、--priority、--label(可重复)、--assignee-id。
--duration 接受整数分钟数,或字符串 NONE / REMINDER(输入时不区分大小写,输出时规范化为大写)。
自动调度是三态的,区别很重要:
- 省略两个标志 → 字段在请求中完全省略(不变)
--auto-scheduled '{"deadlineType":"HARD"}'→ 开启自动调度;嵌套字段包括startDate、deadlineType(HARD/SOFT/NONE)和schedule(默认为Work Hours)--auto-scheduled-off→ 发送显式的null,关闭自动调度
任务的状态必须启用自动调度,否则 API 会拒绝。
任务响应的样子如下——已修剪为下方日历工作流所依赖的字段:
{
"id": "tsk_123",
"name": "Draft Q4 plan",
"description": "Outline the three scenarios.",
"status": { "name": "In Progress", "isResolvedStatus": false },
"priority": "HIGH",
"labels": ["q4"],
"assignees": [{ "id": "usr_1", "name": "Sam Patel" }],
"project": { "id": "prj_1", "name": "Planning" },
"dueDate": "2026-11-01T17:00:00.000Z",
"duration": 45,
"scheduledStart": "2026-10-28T09:00:00.000Z",
"startOn": "2026-10-28",
"completedTime": null
}
解读:status 是一个对象而不是字符串——你需要的过滤器是 status.isResolvedStatus,任务完成或取消后该值变为 true。assignees 和 labels 是数组;project 是对象或 null。scheduledStart 和 dueDate 是 UTC 时间;startOn 是纯日期。
没有日历端点。 任务在日历上的位置是其 scheduledStart(UTC ISO 8601),duration 为分钟数,startOn 为纯日期。要回答“接下来是什么”,请获取任务并按 scheduledStart 排序——不要去寻找返回事件的日程端点,因为 schedules list 只返回工作时间窗口(见下文),而不是已预订的项目。过滤掉 status.isResolvedStatus 任务以排除任何已完成或已取消的任务。
项目
| 命令 | 端点 |
|---|---|
projects list [--workspace-id …] | GET /v1/projects |
projects get <project_id> | GET /v1/projects/{id} |
projects create --name … --workspace-id … | POST /v1/projects |
projects create 还接受 --due-date、--description(接受 HTML)、--label(可重复)、--priority(服务器端默认为 MEDIUM),以及模板对 --project-definition-id + --stages。阶段必须与定义的数量和顺序完全匹配,否则 API 返回 400。对于较长的阶段数组,请使用 --stages @stages.json。CLI 不强制要求这对参数;API 会拒绝没有定义 ID 的阶段列表。
重复任务
| 命令 | 端点 |
|---|---|
recurring-tasks list --workspace-id … | GET /v1/recurring-tasks |
recurring-tasks create | POST /v1/recurring-tasks |
recurring-tasks delete <id> --yes | DELETE /v1/recurring-tasks/{id} |
recurring-tasks create 需要 --frequency、--name、--workspace-id 和 --assignee-id。可选:--deadline-type(仅限 HARD/SOFT——此处 NONE 被拒绝)、--duration(整数分钟或 REMINDER;此端点不接受 NONE)、--starting-on、--ideal-time(HH:mm)、--schedule(默认为 Work Hours)、--description、--priority(仅限 HIGH/MEDIUM)。
--starting-on 会完全按原样发送给 API——不进行修剪。
--frequency 是 Motion 自有 DSL 中的字符串,而不是对象。 这一点经常让人困惑。词汇表文档位于 https://docs.usemotion.com/cookbooks/frequency/:
- 日期代码:
MO、TU、WE、TH、FR、SA、SU - 每日:
daily_every_day、daily_every_week_day、daily_specific_days_[MO, TU, FR] - 每周:
weekly_any_day、weekly_any_week_day、weekly_specific_days_[MO, FR] - 双周:
biweekly_first_week_any_day、biweekly_first_week_specific_days_[MO, TU]、biweekly_second_week_any_week_day - 按月按星期几:
monthly_first_MO、monthly_second_TU、monthly_last_FR - 按月按日期:
monthly_1、monthly_15、monthly_31(超出范围的日期回退到当月的最后一天) - 按月按周:
monthly_any_day_first_week、monthly_any_week_day_last_week、monthly_last_day_of_month - 按季度:
quarterly_first_day、quarterly_last_week_day、quarterly_any_day_first_week
日期数组必须始终与频率类型配对——单独的 [MO, FR] 无效。
一个完整的调用,因为频率字符串是困难部分:
python3 motion.py recurring-tasks create \
--name "Weekly review" --workspace-id ws_1 --assignee-id usr_1 \
--frequency "weekly_specific_days_[MO,FR]" \
--deadline-type SOFT --duration 30 --ideal-time 16:00 \
--schedule "Work Hours" --priority MEDIUM
注意这里更窄的集合:重复任务只接受 HIGH/MEDIUM 优先级和 HARD/SOFT 截止日期类型,而 API 的其他部分也允许 ASAP/LOW 和 NONE。CLI 强制执行这两项限制。
评论
| 命令 | 端点 |
|---|---|
comments list --task-id … | GET /v1/comments |
comments create --task-id … [--content …] | POST /v1/comments |
--content 是 GitHub Flavored Markdown 格式,返回时是 HTML。它是可选的——没有 --content 时请求体只包含任务 ID。Motion 是否接受空评论未经核实;如果你想说什么,请传递 --content。
自定义字段——注意 /beta/ 前缀
| 命令 | 端点 |
|---|---|
custom-fields list --workspace-id … | GET /beta/workspaces/{workspaceId}/custom-fields |
custom-fields create --workspace-id … --type … --name … | POST /beta/workspaces/{workspaceId}/custom-fields |
custom-fields delete <id> --workspace-id … --yes | DELETE /beta/workspaces/{workspaceId}/custom-fields/{id} |
custom-field-values add-task <task_id> … | POST /beta/custom-field-values/task/{taskId} |
custom-field-values add-project <project_id> … | POST /beta/custom-field-values/project/{projectId} |
custom-field-values delete-from-task <task_id> <value_id> --yes | DELETE /beta/custom-field-values/task/{taskId}/custom-fields/{valueId} |
custom-field-values delete-from-project <project_id> <value_id> --yes | DELETE /beta/custom-field-values/project/{projectId}/custom-fields/{valueId} |
12 种自定义字段类型,完全按原样拼写:text、url、date、person、multiPerson、phone、select、multiSelect、number、email、checkbox、relatedTo。
设置值需要 --custom-field-instance-id(工作区上的字段)、--type 和 --value。--value 是 JSON,因此请正确引用:--value '"hello"' 用于字符串,--value '42' 用于数字,--value '["A","B"]' 用于多选,--value 'true' 用于复选框。
python3 motion.py custom-field-values add-task tsk_123 \
--custom-field-instance-id cfi_1 --type select --value '"Alpha"'
custom-fields create 接受 --metadata 作为 JSON——format(plain/formatted/percent,仅数字)、toggle 用于复选框,options 用于选择字段:
--metadata '{"options":[{"id":"a","value":"Alpha","color":"#ff0000"}]}'
用户、工作区、状态、日程
| 命令 | 端点 |
|---|---|
me | GET /v1/users/me |
users list [--workspace-id …] [--team-id …] | GET /v1/users |
workspaces list [--ids …] | GET /v1/workspaces |
statuses list [--workspace-id …] | GET /v1/statuses |
schedules list | GET /v1/schedules |
workspaces list --ids 是可重复的,--all 会跟踪游标。
users list 必须限定范围。 传递 --workspace-id 或 --team-id;脚本会提前拒绝未限定范围的调用并显示该消息,而不是发送一个肯定会返回 400 的请求。要列出所有人,请遍历你的工作区。
schedules list 返回工作时间窗口,而不是事件。 每个日程都有自己的 timezone 和每日 {start, end} 块数组——且一天可能包含多个块(例如,包含早上和晚上的学校接送日程)。不要将其理解为“用户每天有一个工作块”,也不要将其视为日历;要查看实际预订了什么,请读取任务的 scheduledStart 值。
Raw 逃生口
python3 motion.py raw GET /v1/tasks --query status=Todo --query status=Doing
python3 motion.py raw GET /v1/tasks --query assigneeId=u1 --query includeAllStatuses=true --all
python3 motion.py raw POST /v1/comments --body '{"taskId":"t1","content":"hi"}'
python3 motion.py raw DELETE /v1/tasks/t1 --yes
对于任何命名命令未涵盖的内容,请使用此逃生口。--query KEY=VALUE 是可重复的,重复相同的键会发送多个值。--body 接受 JSON 字符串作为请求体。--all 遵循游标并聚合页面。--yes 抑制删除确认检查。