API de Motion
Cliente completo para la API REST de Motion (usemotion.com) — los 27 endpoints documentados en
tareas, proyectos, tareas recurrentes, comentarios, campos personalizados, usuarios, espacios de trabajo, estados y
horarios, además de un escape hatch raw para cualquier cosa aún no documentada.
Cuándo usar este skill
Úsalo siempre que el trabajo involucre Motion: leer o crear tareas, listar proyectos, publicar comentarios en tareas, gestionar tareas recurrentes, o leer estados de espacios de trabajo, usuarios y horarios.
Credenciales
Una credencial, MOTION_API_KEY. La plataforma la inyecta como variable de entorno cuando
un script se ejecuta con RunWithCredentials — nunca pases una clave en la línea de comandos, y nunca
la imprimas o la hagas eco.
Obtén una clave en Motion en Configuración → API. Motion la muestra solo una vez, así que cópiala inmediatamente. Si las llamadas empiezan a devolver 401, la clave fue revocada o regenerada y necesita ser reemplazada.
Nota: con la entrega de credenciales en modo proxy, la variable de entorno contiene un marcador de posición
no secreto y el valor real se inyecta en las solicitudes a api.usemotion.com. Una credencial recién guardada
puede tardar unos minutos en propagarse; un 401 inmediato que luego tiene éxito es propagación,
no una clave incorrecta.
Convenciones que sigue la API
Estos son los hechos que marcan la diferencia entre una llamada que funciona y una que no.
| Aspecto | Valor |
|---|---|
| URL base | https://api.usemotion.com |
| Cabecera de autenticación | X-API-Key: <clave> |
| Verificación de estado | GET /v1/users/me — devuelve el propietario de la clave |
| Prefijos de ruta | /v1/ para todo excepto campos personalizados, que son /beta/ |
| Límites de tasa | 12 solicitudes/min (individual), hasta 120 (equipos), más en enterprise |
| Descripciones | Github Flavored Markdown |
| Valores de prioridad | ASAP, HIGH, MEDIUM, LOW (tareas recurrentes: solo HIGH/MEDIUM) |
| Tipos de plazo | HARD, SOFT, NONE (tareas recurrentes: solo HARD/SOFT) |
Paginación. Los endpoints de lista toman un cursor y devuelven un envoltorio:
{ "meta": { "nextCursor": "…", "pageSize": 25 }, "tasks": [ … ] }
La clave del envoltorio es meta, y el campo de la siguiente página es nextCursor. Pasa --all en cualquier
comando de lista y el script sigue el cursor por ti, devolviendo un único array plano.
--all se detiene en 100 páginas y luego falla, por lo que una consulta descontrolada no puede bloquear una ejecución —
reduce la consulta si lo ves. Y si una página no es ni un objeto JSON ni un array (truncada, o
una página de error de gateway), la caminata se niega en lugar de devolver lo que tiene: una
lista parcial que parece completa es peor que un error.
Dos endpoints de lista devuelven un array simple sin paginación — GET /v1/statuses y
GET /beta/workspaces/{id}/custom-fields. Todo lo demás está envuelto.
Errores. Las respuestas no 2xx llevan {"message", "error", "statusCode"}. Los scripts
lo imprimen como JSON en stderr y salen con código no cero. 429 se reintenta con retroceso respetando
Retry-After; 400, 401, 403 y 404 se muestran inmediatamente y nunca se reintentan.
Los fallos de validación llevan un array errors[] por campo; el cliente añade esos mensajes
para que se vea la razón real.
User-Agent es obligatorio. El borde de Motion bloquea la firma predeterminada Python-urllib/x.y
con un Cloudflare 403 (browser_signature_banned) antes de que la solicitud llegue a la
API. El cliente siempre envía motion-api-skill/1.0; si escribes tu propia solicitud contra
esta API, establece un User-Agent explícitamente o recibirás un 403 confuso en lugar de una respuesta
real.
Los parámetros de consulta booleanos deben estar en minúsculas. Motion analiza las cadenas de consulta como texto, por lo que un
filtro booleano debe ser el literal true, no True de Python. Equivocarse en esto no es
un no-op silencioso: la API responde con un 400 confuso que habla del valor opuesto
(consulta la sección de diferencias en vivo a continuación). El cliente siempre envía literales en minúsculas.
Referencia de comandos
Cada comando imprime JSON en stdout. python3 motion.py <grupo> --help lista las banderas.
Tareas
| Comando | Endpoint |
|---|---|
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 |
Filtros de tasks list: --assignee-id, --project-id, --workspace-id, --name
(subcadena sin distinción de mayúsculas/minúsculas), --label (singular, un nombre de etiqueta), --status (repetible),
--include-all-statuses. --status y --include-all-statuses son mutuamente excluyentes —
enviar ambos es rechazado por la API, y la CLI se niega temprano con ese mensaje en lugar de
gastar una solicitud en un 400 garantizado.
--include-all-statuses es la forma de ver una tarea que ha pasado su espacio de trabajo predeterminado
(por ejemplo, todo lo que está en Backlog o In Review); sin él, GET /v1/tasks devuelve
solo el estado predeterminado y la lista parece vacía. Combinarlo con --all devuelve cada
tarea asignada a alguien, en todos los estados.
tasks create / tasks update toman los mismos campos: --name, --workspace-id,
--due-date (ISO 8601; requerido para tareas programadas), --duration, --status,
--project-id, --description, --priority, --label (repetible), --assignee-id.
--duration acepta un número entero de minutos, o las cadenas NONE / REMINDER
(sin distinción de mayúsculas/minúsculas en la entrada, normalizado a mayúsculas).
La auto-programación es triestado, y la distinción importa:
- omitir ambas banderas → el campo se omite de la solicitud por completo (sin cambios)
--auto-scheduled '{"deadlineType":"HARD"}'→ lo activa; los campos anidados sonstartDate,deadlineType(HARD/SOFT/NONE) yschedule(predeterminadoWork Hours)--auto-scheduled-off→ envía unnullexplícito, lo desactiva
El estado de la tarea debe tener la auto-programación activada, o la API se niega.
Una respuesta de tarea se ve así — recortada a los campos de los que depende el flujo de trabajo del calendario a continuación:
{
"id": "tsk_123",
"name": "Borrador del plan Q4",
"description": "Esbozar los tres escenarios.",
"status": { "name": "En progreso", "isResolvedStatus": false },
"priority": "HIGH",
"labels": ["q4"],
"assignees": [{ "id": "usr_1", "name": "Sam Patel" }],
"project": { "id": "prj_1", "name": "Planificación" },
"dueDate": "2026-11-01T17:00:00.000Z",
"duration": 45,
"scheduledStart": "2026-10-28T09:00:00.000Z",
"startOn": "2026-10-28",
"completedTime": null
}
Léelo como: status es un objeto, no una cadena — el filtro que quieres es
status.isResolvedStatus, que se vuelve true una vez que una tarea está completada o cancelada.
assignees y labels son arrays; project es un objeto o null. scheduledStart
y dueDate están en UTC; startOn es una fecha simple.
No hay un endpoint de calendario. La posición de una tarea en el calendario es su scheduledStart
(UTC ISO 8601), con duration en minutos y startOn como la fecha simple. Para responder "qué
viene después", obtén las tareas y ordena por scheduledStart — no busques un endpoint
de horario que devuelva eventos, porque schedules list devuelve solo las ventanas de horas laborables
(ver más abajo), no los elementos reservados. Filtra las tareas con status.isResolvedStatus para
excluir cualquier cosa ya completada o cancelada.
Proyectos
| Comando | Endpoint |
|---|---|
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 también toma --due-date, --description (se acepta HTML), --label
(repetible), --priority (por defecto es MEDIUM en el servidor), y el par de plantillas
--project-definition-id + --stages. Las etapas deben coincidir exactamente con el orden y recuento
de la definición, o la API devuelve 400. Para arrays largos de etapas usa --stages @stages.json.
La CLI no aplica el par; la API rechaza una lista de etapas sin un id de definición.
Tareas recurrentes
| Comando | Endpoint |
|---|---|
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 requiere --frequency, --name, --workspace-id y
--assignee-id. Opcional: --deadline-type (solo HARD/SOFT — NONE se rechaza aquí),
--duration (minutos como entero, o REMINDER; NONE no se acepta en este
endpoint), --starting-on, --ideal-time (HH:mm), --schedule (predeterminado Work Hours),
--description, --priority (solo HIGH/MEDIUM).
--starting-on se envía a la API exactamente como se da — sin recortes.
--frequency es una cadena en el DSL propio de Motion, no un objeto. Esto confunde a la gente.
El vocabulario está documentado en https://docs.usemotion.com/cookbooks/frequency/:
- Códigos de día:
MO,TU,WE,TH,FR,SA,SU - Diario:
daily_every_day,daily_every_week_day,daily_specific_days_[MO, TU, FR] - Semanal:
weekly_any_day,weekly_any_week_day,weekly_specific_days_[MO, FR] - Quincenal:
biweekly_first_week_any_day,biweekly_first_week_specific_days_[MO, TU],biweekly_second_week_any_week_day - Mensual por día de semana:
monthly_first_MO,monthly_second_TU,monthly_last_FR - Mensual por fecha:
monthly_1,monthly_15,monthly_31(un día fuera de rango retrocede al último día del mes) - Mensual por semana:
monthly_any_day_first_week,monthly_any_week_day_last_week,monthly_last_day_of_month - Trimestral:
quarterly_first_day,quarterly_last_week_day,quarterly_any_day_first_week
Un array de días siempre debe ir emparejado con un tipo de frecuencia — [MO, FR] solo no es válido.
Una llamada completa, ya que la cadena de frecuencia es la parte difícil:
python3 motion.py recurring-tasks create \
--name "Revisión semanal" --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
Observa los conjuntos más restringidos aquí: las tareas recurrentes solo toman prioridad HIGH/MEDIUM y solo
tipos de plazo HARD/SOFT, mientras que el resto de la API también permite ASAP/LOW y NONE.
La CLI aplica ambos.
Comentarios
| Comando | Endpoint |
|---|---|
comments list --task-id … | GET /v1/comments |
comments create --task-id … [--content …] | POST /v1/comments |
--content es Github Flavored Markdown al entrar y vuelve como HTML. Es
opcional — sin --content el cuerpo de la solicitud lleva solo el id de la tarea. Si Motion
acepta un comentario vacío no está verificado; pasa --content cuando quieras decir algo.
Campos personalizados — nota el prefijo /beta/
| Comando | Endpoint |
|---|---|
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} |
Los 12 tipos de campos personalizados, exactamente como se escriben: text, url, date, person,
multiPerson, phone, select, multiSelect, number, email, checkbox, relatedTo.
Para establecer un valor necesita --custom-field-instance-id (el campo en el espacio de trabajo), --type,
y --value. --value es JSON, así que cítalo correctamente: --value '"hola"' para una cadena,
--value '42' para un número, --value '["A","B"]' para una selección múltiple, --value 'true' para
una casilla de verificación.
python3 motion.py custom-field-values add-task tsk_123 \
--custom-field-instance-id cfi_1 --type select --value '"Alfa"'
custom-fields create toma --metadata como JSON — format (plain/formatted/percent,
solo números), toggle para casillas de verificación, y options para campos de selección:
--metadata '{"options":[{"id":"a","value":"Alfa","color":"#ff0000"}]}'
Usuarios, espacios de trabajo, estados, horarios
| Comando | Endpoint |
|---|---|
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 es repetible, y --all sigue el cursor.
users list debe tener un alcance. Pasa --workspace-id o --team-id; el script rechaza una
llamada sin alcance al principio con ese mensaje en lugar de gastar una solicitud en el 400 que la API
devuelve. Para listar a todos, itera sobre tus espacios de trabajo.
schedules list devuelve ventanas de horas laborables, no eventos. Cada horario tiene su propia
timezone y un array por día de bloques {start, end} — y un día puede contener múltiples
bloques (por ejemplo, un horario de recogida escolar con uno matutino y otro vespertino). No leas esto como
"el usuario tiene un bloque de trabajo al día", y no lo trates como el calendario; para lo que
está realmente reservado, lee los valores scheduledStart de las tareas.
Escape hatch 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":"hola"}'
python3 motion.py raw DELETE /v1/tasks/t1 --yes
Úsalo para cualquier cosa que los comandos nombrados no cubran. --query KEY=VALUE es repetible y repetir la misma
clave añade múltiples parámetros de consulta con el mismo nombre (tal como la API espera para filtros repetibles).
--body debe ser JSON crudo entre comillas. Toda operación de escritura o eliminación requiere --yes.
Se aplican las mismas reglas de paginación: el parámetro --all recorre las páginas con el cursor; la caminata se niega
si encuentra una página que no es JSON, y se detiene en 100 páginas.
Comportamiento de la línea de comandos
Todas las eliminaciones requieren --yes. Si falta, el script imprime "No confirmaste
y las cosas peligrosas no se ejecutan. Agrega --yes para confirmar." y sale con código 1 —
el mensaje exacto está cableado así que un agente que lo lea puede saber qué hacer.
Para ignorar errores de validación de la API (no recomendado para scripting), pasa --force
— esto envía la solicitud incluso si faltan argumentos requeridos en el lado de la CLI. La
API aún los rechazará si son realmente obligatorios; útil cuando el comando de lista tiene argumentos que
solo se aplican a una sub-ruta.
--help imprime ayuda de uso y sale inmediatamente. El texto proviene del módulo argparse de Python y
no está traducido; el agente debe leer las banderas, no la prosa inglesa.
Diferencias en vivo (gotchas de la API Motion)
- Los filtros booleanos deben estar en minúsculas.
?includeAllStatuses=Truedevuelve un400diciendo "Se esperabafalse" — aunque literalmente enviasteTrue. Solotrue/falsefuncionan. El cliente siempre envía minúsculas. - Calendario vs. horario. No existe un endpoint de calendario.
schedules listdevuelve ventanas, no eventos. Para ver qué está programado en un día, lee las tareas. statuses un objeto. No se puede filtrar porstatus: "Done"; en su lugar, usastatus.isResolvedStatus.completedTimepuede sernull. No está ausente del JSON; es explícitamentenullpara tareas no completadas.- El filtro
nameentasks listes una subcadena sin distinción de mayúsculas/minúsculas. No una coincidencia exacta, no una expresión regular. - Los límites de tasa son estrictos. 429 se reintenta automáticamente con retroceso. En pruebas locales, espera al menos 5 segundos entre llamadas para mantenerte por debajo del límite de 12/min.
- El espacio de trabajo y el proyecto pueden divergir. Los ids de tarea pertenecen a un espacio de trabajo y una tarea puede referirse a un proyecto en otro espacio de trabajo (o ninguno).
- Repetir un filtro es correcto.
--status A --status Bse envía comostatus=A&status=By la API lo entiende como "A o B". La repetición simple es la forma prevista de OR en Motion.
Sin solicitudes entrantes (estilo teléfono)
Este skill no maneja webhooks ni eventos de Motion "push". Un agente solo puede leer Motion cuando ejecuta un script. No hay forma de recibir una notificación cuando una tarea cambia.
Registro de cambios
Se han eliminado algunos errores de comandos anteriores: los filtros repetibles se envían correctamente, los
valores booleanos están siempre en minúsculas, y el escape hatch raw maneja --body correctamente.
tasks delete ahora requiere el flag --yes en lugar de preguntar interactivamente.
El script nunca imprime la clave API.
Pruebas
Se incluye un conjunto de pruebas en test_motion.py (unittest). Ejecuta python3 -m pytest test_motion.py -v
desde este directorio. Las pruebas se burlan de todas las llamadas HTTP, no golpean el servidor real
y no necesitan una clave API. Cubren entradas inválidas, casos límite de paginación y
cada comando al menos una vez.