Ir al contenido

Tareas recurrentes y repetidas

Bordio tiene dos formas diferentes en que una tarea puede repetirse. Son mutuamente excluyentes — elige una:

  • Según un horario (recurrence) — la tarea/evento se repite en fechas fijas, p. ej. cada lunes. Usa un RRULE de iCalendar. Funciona para tareas y eventos. Se trata primero, más abajo.
  • Después de la finalización (repeat_on_completion) — se crea una copia nueva solo cuando cierras la tarea, con las fechas desplazadas hacia adelante. Ideal para “hazlo de nuevo N días después de terminarlo”. Solo tareas. Consulta Repetir después de la finalización.

Una tarea o evento puede repetirse según un horario — una tarea diaria, una reunión semanal, una revisión mensual. Creas una serie repetida añadiendo un objeto recurrence a la solicitud de creación normal. El resultado es una serie (una plantilla que direccionas por su id), no un montón de copias.

La recurrencia usa el formato RRULE de iCalendar (RFC 5545) — la misma sintaxis de reglas que Google Calendar y CalDAV. Si alguna vez has construido una recurrencia con una biblioteca de calendario, ya la conoces.

Añade recurrence a POST /scheduled-events. La serie se ancla en start_at.

Terminal window
curl -X POST "https://api.bordio.com/public/v1/scheduled-events" \
-H "Authorization: Bearer brd_sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"title": "Team standup",
"start_at": "2026-07-06T09:00:00Z",
"end_at": "2026-07-06T09:15:00Z",
"recurrence": {
"rule": "RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR",
"until": "2026-12-31"
}
}'

Esto crea una reunión cada lunes, miércoles y viernes desde el 06 de julio de 2026 hasta fin de año.

Añade recurrence a POST /tasks. La serie se ancla en la date de la tarea (el día en que la tarea está programada) — debes incluir date, de lo contrario la solicitud se rechaza con 422.

Terminal window
curl -X POST "https://api.bordio.com/public/v1/tasks" \
-H "Authorization: Bearer brd_sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"title": "Water the plants",
"project_id": "proj_6594a1b2c3d4e5f6a7b8c9d0",
"date": "2026-07-06",
"recurrence": {
"rule": "RRULE:FREQ=DAILY",
"until": "2026-08-31"
}
}'
Campo Obligatorio Descripción
rule sí Una cadena RRULE de iCalendar simple, p. ej. RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR.
until no Fecha/fecha-hora ISO-8601 — el final inclusivo de la serie.

El rule es una línea RRULE estándar. Las partes más comunes:

  • FREQ= — con qué frecuencia: DAILY, WEEKLY, MONTHLY o YEARLY.
  • INTERVAL= — cada N períodos (p. ej. INTERVAL=2 con FREQ=WEEKLY = cada dos semanas). El valor predeterminado es 1.
  • BYDAY= — días de la semana: MO,TU,WE,TH,FR,SA,SU (úsalo con FREQ=WEEKLY).
  • BYMONTHDAY= — día del mes, p. ej. BYMONTHDAY=1 (úsalo con FREQ=MONTHLY).

Ejemplos:

Objetivo rule
Cada día RRULE:FREQ=DAILY
Cada día laborable RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
Un lunes sí y otro no RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=MO
El primero de cada mes RRULE:FREQ=MONTHLY;BYMONTHDAY=1
Cada año RRULE:FREQ=YEARLY

Para mantener las series válidas y seguras, la API es estricta con el rule:

  • FREQ es obligatorio y debe ser uno de DAILY, WEEKLY, MONTHLY, YEARLY. Las frecuencias inferiores a un día (HOURLY, MINUTELY, SECONDLY) se rechazan.
  • No incluyas DTSTART. El inicio se deriva del recurso (start_at para eventos, date para tareas). Un DTSTART en el rule se rechaza.
  • No uses COUNT. Limita la serie con el campo until en su lugar.
  • La serie no puede empezar más de 730 días en el pasado (start_at en eventos, date en tareas). El historial más antiguo se importa como registros sueltos o como una serie anclada en una fecha reciente; las series existentes no se ven afectadas.
  • INTERVAL, si está presente, debe ser un entero positivo (≤ 1000).
  • El rule debe ser una única línea RRULE: (sin RDATE/EXDATE, sin saltos de línea).

Cualquier otra cosa devuelve 422 validation_error con el código invalid_recurrence_rule.

Usa el campo until para la fecha de finalización. Si omites until, la serie es abierta y se repite indefinidamente.

Los recursos recurrentes incluyen un objeto recurrence en las respuestas; los individuales (no recurrentes) devuelven recurrence: null.

{
"data": {
"id": "event_6594a1b2c3d4e5f6a7b8c9d0",
"title": "Team standup",
"start_at": "2026-07-06T09:00:00.000Z",
"recurrence": {
"rule": "RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR",
"until": "2026-12-31T00:00:00.000Z"
}
}
}

El rule devuelto está normalizado a la línea RRULE: — el DTSTART interno no se expone.

Repetir después de la finalización (solo tareas)

Sección titulada «Repetir después de la finalización (solo tareas)»

En lugar de un horario fijo, una tarea puede generar una nueva copia de sí misma cuando la completas (moverla a un estado cerrado). Este es un mecanismo diferente del recurrence anterior — no hay RRULE, y no se crea nada hasta que la tarea se cierra realmente. Añade repeat_on_completion a POST /tasks:

Terminal window
curl -X POST "https://api.bordio.com/public/v1/tasks" \
-H "Authorization: Bearer brd_sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"title": "Change the water filter",
"project_id": "proj_6594a1b2c3d4e5f6a7b8c9d0",
"estimated_time_seconds": 1800,
"repeat_on_completion": {
"start_date_shift_days": 30,
"due_date_shift_days": 2,
"until": "2027-12-31"
}
}'

Cuando esta tarea se cierra, Bordio crea una copia nueva: el mismo título, proyecto, asignados, prioridad y subtareas, con su fecha de inicio establecida 30 días después de la fecha de finalización y su fecha de vencimiento 2 días después de esa. La copia se repite de la misma manera, hasta que pasa until.

Campo Obligatorio Descripción
start_date_shift_days no Días después de la finalización en que empieza la siguiente copia. 0 = el mismo día. Omítelo o pasa null para dejar la copia sin fecha — irá a la waiting list.
due_date_shift_days no Días después del inicio de la copia en que cae su fecha de vencimiento. Omítelo para no tener fecha de vencimiento. Se ignora cuando la copia no tiene fecha de inicio.
until no Deja de repetir después de esta fecha (ISO-8601). Omítelo para repetir indefinidamente.
initial_task_status_id no Estado en el que empieza cada nueva copia (task_status_...). Por defecto, el estado de la tarea — defínelo para que la copia empiece, por ejemplo, en “To do” en lugar del estado cerrado que la disparó.

Notas:

  • Solo cuando se cierra. La copia se crea cuando la tarea pasa a un estado cuya condición es cerrada — no en cada edición.
  • Los campos se heredan. El título, la descripción, el proyecto, los asignados, los reporteros, la prioridad, las subtareas y la estimación de tiempo se transfieren automáticamente a cada copia; tú solo especificas los desplazamientos de fecha.
  • Repetición en la waiting list. Sin start_date_shift_days, la copia no tiene fecha de inicio y espera en la waiting list hasta que alguien la programe. Como la fecha de vencimiento se calcula a partir de la fecha de inicio, due_date_shift_days no tiene efecto en este modo.
  • Mutuamente excluyente con recurrence. Enviar ambos en la misma tarea devuelve 422.

Las respuestas incluyen un objeto repeat_on_completion (o null):

{
"data": {
"id": "task_6594a1b2c3d4e5f6a7b8c9d0",
"repeat_on_completion": {
"start_date_shift_days": 30,
"due_date_shift_days": 2,
"until": "2027-12-31T00:00:00.000Z"
}
}
}
  • La serie (la propia tarea recurrente) tiene un id simple — task_<id> / event_<id> — el id que recibes al crearla. Leerla, actualizarla o eliminarla se aplica a toda la serie.
  • Una ocurrencia individual tiene su propio id que lleva la fecha de la ocurrencia, p. ej. task_<id>_20260113T090000Z (consulta Identificadores). Léelo de vuelta desde la API y úsalo tal cual para direccionar esa única ocurrencia — nunca lo construyas tú mismo.

GET /tasks responde a dos preguntas distintas, y cuál de ellas obtienes depende del parámetro view.

Sin view, la lista contiene tus tareas puntuales más las ocurrencias individuales que ya existen en el sistema: las que alguien editó, completó o reprogramó. La tarea recurrente en sí no está en esa lista, y una misma serie puede aparecer como varias filas. Es el comportamiento histórico y no cambia.

Con view=by_task, la lista contiene tareas: una tarea recurrente es una sola fila, con su regla y la ocurrencia de la que toca ocuparse. Sus ocurrencias no se listan por separado.

Terminal window
curl "https://api.bordio.com/public/v1/tasks?view=by_task&project_id=proj_6594a1b2c3d4e5f6a7b8c9d0" \
-H "Authorization: Bearer brd_sk_live_..."
{
"data": [
{
"id": "task_6594a1b2c3d4e5f6a7b8c9d0",
"title": "Daily standup",
"kind": "recurring_task",
"series_id": "task_6594a1b2c3d4e5f6a7b8c9d0",
"series_chain_id": "task_6594a1b2c3d4e5f6a7b8c9d0",
"recurrence": { "rule": "RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR", "until": null },
"recurrence_summary": "every week on Monday, Wednesday, Friday",
"current_occurrence": {
"id": "task_6594a1b2c3d4e5f6a7b8c9d0_20260907T090000Z",
"kind": "occurrence",
"title": "Daily standup",
"task_status_id": "task_status_6594a1b2c3d4e5f6a7b8c9d2"
}
}
]
}

Estos campos están presentes en toda tarea que devuelve la API, en cualquier modo — se añadieron junto a los existentes, así que un cliente que no los lea no nota ningún cambio:

Campo Significado
kind task (puntual), recurring_task (se repite según una regla), occurrence (una ocurrencia con fecha)
series_id La tarea recurrente a la que pertenece el registro. Una tarea recurrente lleva aquí su propio id; una puntual, null
series_chain_id Agrupa las tareas recurrentes que provienen de una misma serie original — consulta Dividir una serie
recurrence_summary La misma regla en palabras, p. ej. every weekday
current_occurrence La ocurrencia de la que toca ocuparse, completa. null cuando a la tarea recurrente no le quedan ocurrencias abiertas

current_occurrence es la ocurrencia abierta más antigua, incluidas las vencidas: en una tarea diaria abandonada verás lo más viejo que sigue pendiente, no la copia de mañana. Llega como objeto de tarea completo, porque una ocurrencia que alguien editó puede diferir de la serie — su propio título, responsables, estatus o fecha límite — y no deberías necesitar otra petición para enterarte.

Las integraciones nuevas deberían pasar view siempre de forma explícita.

Listar ocurrencias día a día (view=by_date)

Sección titulada «Listar ocurrencias día a día (view=by_date)»

La tercera pregunta que puede responder una lista es «qué ocurre entre estas fechas». Pasa view=by_date con un rango de fechas y cada día en el que cae la tarea recurrente será su propio registro — incluidas las fechas que nadie ha tocado, que no existen como registros separados hasta que las pides:

Terminal window
curl "$BASE/tasks?view=by_date&date_from=2026-09-01&date_to=2026-09-30" -H "$AUTH"
{
"data": [
{ "id": "task_..._20260901T090000Z", "kind": "occurrence", "date": "2026-09-01", "title": "Team standup" },
{ "id": "task_..._20260902T090000Z", "kind": "occurrence", "date": "2026-09-02", "title": "Team standup" }
],
"pagination": { "next_cursor": "…", "has_more": true }
}

Lo que conviene saber de esta vista:

  • el rango es obligatorio y no puede superar los 31 días. Las ocurrencias futuras se generan a partir de la regla, así que una petición sin límite no tiene final;
  • el rango se aplica a la date de la tarea — el día para el que está programada — y no a su fecha límite. due_date_from / due_date_to siguen funcionando como filtros normales dentro de la vista;
  • las tareas puntuales programadas en el rango se listan junto a las ocurrencias; una tarea sin fecha no aparece (no hay día en el que ponerla);
  • una ocurrencia que alguien editó aparece una sola vez, como el registro que realmente es;
  • los resultados van desde el inicio del rango y la paginación funciona como siempre: next_cursor hasta que has_more sea false. Los cursores no son intercambiables entre vistas: el cursor de una lista lo rechaza la otra con 400;
  • closed_since no se combina con ella (400): ese filtro ordena por momento de finalización, y una lista no puede tener dos órdenes. Pregunta por el trabajo terminado aparte;
  • en una petición se expanden como máximo 500 tareas recurrentes.

date_from / date_to también funcionan sin view, como un filtro normal por el día para el que está programada la tarea.

Saber cuál es la próxima ocurrencia justo al cerrar una

Sección titulada «Saber cuál es la próxima ocurrencia justo al cerrar una»

PATCH acepta el mismo parámetro view. Con view=by_task, cerrar una ocurrencia devuelve la propia tarea recurrente, con current_occurrence ya apuntando a la siguiente — sin necesidad de otra petición:

Terminal window
curl -X PATCH "https://api.bordio.com/public/v1/tasks/task_..._20260907T090000Z?view=by_task" \
-H "Authorization: Bearer brd_sk_live_..." \
-H "Content-Type: application/json" \
-d '{"task_status_id": "task_status_..."}'

Sin view, la respuesta es el registro que modificaste, como antes.

Editar “esta y las siguientes ocurrencias” deja dos tareas recurrentes: la parte anterior al cambio y la posterior. Así se comportan las aplicaciones de calendario, y por eso un proyecto puede contener dos filas que parecen el mismo standup.

series_chain_id indica que van juntas:

  • editar propiedades (título, responsable, descripción) mantiene ambas mitades en la misma cadena — mismo series_chain_id, distinto series_id;
  • editar la hora o la regla de repetición inicia una serie realmente nueva, y la nueva mitad recibe su propio series_chain_id.

Una mitad que se quedó sin ocurrencias sigue existiendo (sus ocurrencias pasadas son historia real), pero su current_occurrence es null.

En la vista by_task, state pregunta si hay trabajo pendiente en lugar de mirar un campo de estatus:

Terminal window
curl "$BASE/tasks?view=by_task&state=open" -H "$AUTH"

Una tarea recurrente cuenta como abierta mientras le quede una ocurrencia abierta, y como cerrada cuando no queda ninguna — sin importar el estatus con el que arranca cada ocurrencia. Sin view=by_task, el filtro trabaja sobre el estatus propio del registro, como siempre.

Lo completado recientemente es otra pregunta con otra respuesta: las ocurrencias completadas son registros individuales, así que se listan sin view=by_task:

Terminal window
curl "$BASE/tasks?closed_since=2026-07-28" -H "$AUTH"

Ambos filtros en contexto, en Filtrar tareas.

GET /scheduled-events acepta el mismo parámetro view, con el mismo significado:

view Un registro es
omitido / by_date una ocurrencia — el comportamiento por defecto, sin cambios
by_task un evento; el recurrente lleva su regla y, en current_occurrence, la próxima ocurrencia dentro del rango solicitado
Terminal window
curl "$BASE/scheduled-events?from=2026-09-01T00:00:00Z&to=2026-09-30T00:00:00Z&view=by_task" -H "$AUTH"

El rango from / to sigue siendo obligatorio en ambas vistas. Los eventos llevan los mismos campos que las tareas — kind (event, recurring_event, occurrence), series_id, series_chain_id, recurrence_summary, current_occurrence — así que un mismo código cliente puede leer ambos.

  • Crear una tarea o evento recurrente.
  • Leer, actualizar y eliminar tanto una serie como una ocurrencia individual por su id.
  • Listar tareas y eventos recurrentes como tareas y eventos (view=by_task), con la siguiente ocurrencia incluida.
  • Listar ocurrencias en un rango de fechas — view=by_date en tareas y la lista por defecto en eventos.
  • Elegir a qué afecta una edición con scope — ver más abajo.

Editar una ocurrencia, las siguientes o todas

Sección titulada «Editar una ocurrencia, las siguientes o todas»

PATCH y DELETE aceptan el parámetro de consulta scope:

scope A qué afecta
omitido / only_this solo la ocurrencia que direccionaste — el valor por defecto y el comportamiento anterior
this_and_following esa ocurrencia y todas las posteriores; divide la tarea recurrente en dos
all todas las ocurrencias, incluidas las pasadas
Terminal window
# Renombrar el resto de la serie sin tocar el historial
curl -X PATCH "https://api.bordio.com/public/v1/tasks/task_..._20260907T090000Z?scope=this_and_following" \
-H "Authorization: Bearer brd_sk_live_..." \
-H "Content-Type: application/json" \
-d '{"title": "Standup (asíncrono)"}'

En una tarea que no se repite, el parámetro se ignora.

Direccionar la tarea recurrente en sí exige scope

Sección titulada «Direccionar la tarea recurrente en sí exige scope»

Si el id al que aplicas PATCH o DELETE es la tarea recurrente completa (el series_id, no un id de ocurrencia) y no pasaste scope, la petición se rechaza con 400. Renombrar o borrar una serie entera por accidente no tiene vuelta atrás, y los dos identificadores se parecen: por eso la API te pide precisar qué querías — pasa scope=all o direcciona una ocurrencia concreta por su propio id.

Qué puede y qué no puede cambiar scope=all

Sección titulada «Qué puede y qué no puede cambiar scope=all»

scope=all escribe los campos que envías en toda la serie y deja el resto intacto: si alguien movió un martes o reasignó una fecha suelta, eso se mantiene.

Algunos campos no pueden extenderse por la serie y se rechazan con 400 en lugar de aplicarse a medias:

Campo Por qué
assignee_ids, custom_fields contienen identificadores propios de cada registro; copiarlos entre ocurrencias corrompería los registros
tag_ids las etiquetas se aplican por su propio mecanismo, con su propio comportamiento ante la recurrencia
due_date cada ocurrencia deriva su fecha límite de su propia fecha — no hay un valor común a la serie

Cambiar la programación (la fecha o la regla de repetición) es this_and_following, no all: divide la serie en lugar de reescribir su pasado.

DELETE ?scope=all elimina la tarea recurrente junto con todas sus ocurrencias.