Перейти к содержимому

Повторяющиеся задачи и события

В Bordio есть два разных способа повторения задачи. Они взаимоисключающие — выберите один:

  • По расписанию (recurrence) — задача/событие повторяется в фиксированные даты, например каждый понедельник. Использует iCalendar RRULE. Работает для задач и событий. Рассматривается первым, ниже.
  • После завершения (repeat_on_completion) — новая копия создаётся только когда вы закрываете задачу, со сдвигом дат вперёд. Подходит для «сделать это снова через N дней после того, как я закончу». Только задачи. См. Повтор после завершения.

Задача или событие может повторяться по расписанию — ежедневная задача, еженедельный стендап, ежемесячный обзор. Вы создаёте повторяющуюся серию, добавляя объект recurrence в обычный запрос на создание. Результат — это серия (один шаблон, к которому вы обращаетесь по его id), а не набор копий.

Повторение использует формат iCalendar RRULE (RFC 5545) — тот же синтаксис правил, что и в Google Calendar и CalDAV. Если вы когда-либо создавали повторение с помощью календарной библиотеки, вы уже с ним знакомы.

Добавьте recurrence в POST /scheduled-events. Серия привязывается к 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"
}
}'

Это создаёт стендап каждый понедельник, среду и пятницу с 06 июля 2026 года до конца года.

Добавьте recurrence в POST /tasks. Серия привязывается к date задачи (день, на который назначена задача) — вы обязаны указать date, иначе запрос будет отклонён с ошибкой 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"
}
}'
Поле Обязательно Описание
rule да Чистая строка iCalendar RRULE, например RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR.
until нет Дата/дата-время в формате ISO-8601 — включительная граница окончания серии.

rule — это стандартная строка RRULE. Наиболее часто используемые части:

  • FREQ= — как часто: DAILY, WEEKLY, MONTHLY или YEARLY.
  • INTERVAL= — каждые N периодов (например, INTERVAL=2 с FREQ=WEEKLY = раз в две недели). По умолчанию 1.
  • BYDAY= — дни недели: MO,TU,WE,TH,FR,SA,SU (используется с FREQ=WEEKLY).
  • BYMONTHDAY= — день месяца, например BYMONTHDAY=1 (используется с FREQ=MONTHLY).

Примеры:

Цель rule
Каждый день RRULE:FREQ=DAILY
Каждый будний день RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
Каждый второй понедельник RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=MO
Первое число каждого месяца RRULE:FREQ=MONTHLY;BYMONTHDAY=1
Каждый год RRULE:FREQ=YEARLY

Чтобы серии оставались валидными и безопасными, API строг к rule:

  • FREQ обязателен и должен быть одним из DAILY, WEEKLY, MONTHLY, YEARLY. Частоты чаще, чем раз в день (HOURLY, MINUTELY, SECONDLY), отклоняются.
  • Не включайте DTSTART. Начало выводится из ресурса (start_at для событий, date для задач). DTSTART в rule отклоняется.
  • Не используйте COUNT. Вместо этого ограничивайте серию полем until.
  • Серия не может начинаться раньше, чем 730 дней назад (start_at у событий, date у задач). Более старую историю переносят разовыми записями или серией с недавней датой; существующих серий это не касается.
  • INTERVAL, если присутствует, должен быть положительным целым числом (≤ 1000).
  • rule должен быть единственной строкой RRULE: (без RDATE/EXDATE, без переносов строк).

Всё остальное возвращает 422 validation_error с кодом invalid_recurrence_rule.

Используйте поле until для даты окончания. Если вы опустите until, серия будет бессрочной и будет повторяться неограниченно.

Повторяющиеся ресурсы включают в ответах объект recurrence; одиночные (не повторяющиеся) возвращают 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"
}
}
}

Возвращаемый rule нормализуется до строки RRULE: — внутренний DTSTART не раскрывается.

Вместо фиксированного расписания задача может порождать новую копию себя, когда вы её завершаете (переводите в закрытый статус). Это иной механизм, нежели recurrence выше — здесь нет RRULE, и ничего не создаётся, пока задача действительно не закрыта. Добавьте repeat_on_completion в 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"
}
}'

Когда эта задача закрывается, Bordio создаёт новую копию: тот же заголовок, проект, исполнители, приоритет и подзадачи, с датой начала, установленной через 30 дней после даты завершения, и датой выполнения через 2 дня после этого. Копия повторяется тем же образом, пока не пройдёт until.

Поле Обязательно Описание
start_date_shift_days нет Через сколько дней после завершения начинается следующая копия. 0 = в тот же день. Опустите или передайте null, чтобы копия осталась без даты — она попадёт в waiting list.
due_date_shift_days нет Через сколько дней после начала копии наступает её срок выполнения. Опустите, чтобы не было срока выполнения. Игнорируется, если у копии нет даты начала.
until нет Прекратить повторение после этой даты (ISO-8601). Опустите для бессрочного повторения.
initial_task_status_id нет Статус, в котором стартует каждая новая копия (task_status_...). По умолчанию — статус задачи; задайте, чтобы копия начиналась, например, с «To do», а не с закрывшего её статуса.

Примечания:

  • Только при закрытии. Копия создаётся, когда задача переходит в статус, состояние которого — закрытое, а не при каждом изменении.
  • Поля наследуются. Заголовок, описание, проект, исполнители, репортеры, приоритет, подзадачи и оценка времени переносятся в каждую копию автоматически; вы указываете только сдвиги дат.
  • Повтор в waiting list. Без start_date_shift_days копия не имеет даты начала и ждёт в waiting list, пока её не запланируют. Так как срок выполнения отсчитывается от даты начала, due_date_shift_days в этом режиме не действует.
  • Взаимоисключимо с recurrence. Отправка обоих для одной задачи возвращает 422.

Ответы включают объект repeat_on_completion (или 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"
}
}
}
  • Серия (сама повторяющаяся задача) имеет обычный id — task_<id> / event_<id> — тот id, который вы получаете при создании. Чтение, обновление или удаление применяется ко всей серии.
  • Отдельное вхождение имеет собственный id, содержащий дату вхождения, например task_<id>_20260113T090000Z (см. Идентификаторы). Считайте его из API и используйте дословно для обращения к этому единственному вхождению — никогда не собирайте его самостоятельно.

GET /tasks отвечает на два разных вопроса, и какой именно ответ вы получите — решает параметр view.

Без view список содержит разовые задачи плюс те отдельные вхождения, которые уже существуют в системе: те, что кто-то редактировал, завершил или перенёс. Самой повторяющейся задачи в этом списке нет, а одна серия может дать несколько строк. Это историческое поведение, и оно не меняется.

С view=by_task список содержит задачи: повторяющаяся задача — это одна строка с её правилом и ближайшим вхождением, которым предстоит заняться. Отдельными строками её вхождения не выводятся.

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": "Ежедневный стендап",
"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": "Ежедневный стендап",
"task_status_id": "task_status_6594a1b2c3d4e5f6a7b8c9d2"
}
}
]
}

Эти поля есть у каждой задачи, которую отдаёт API, в любом режиме — они добавлены рядом с существующими, поэтому клиент, который их не читает, изменений не заметит:

Поле Значение
kind task (разовая), recurring_task (повторяется по правилу), occurrence (одно вхождение с датой)
series_id Повторяющаяся задача, которой принадлежит запись. У самой повторяющейся задачи здесь её собственный id; у разовой — null
series_chain_id Связывает повторяющиеся задачи, произошедшие от одной исходной серии — см. Разделение серии
recurrence_summary То же правило словами, например every weekday
current_occurrence Вхождение, которым предстоит заняться, целиком. null, когда открытых вхождений у повторяющейся задачи не осталось

current_occurrence — это самое раннее открытое вхождение, включая просроченные: у заброшенной ежедневной задачи вы увидите самое старое невыполненное, а не завтрашнюю копию. Оно приходит полным объектом задачи, потому что отредактированное вхождение может отличаться от серии — своим названием, исполнителями, статусом или дедлайном, — и узнавать об этом отдельным запросом не нужно.

Новым интеграциям стоит всегда передавать view явно.

Третий вопрос, на который отвечает список, — «что происходит между этими датами». Передайте view=by_date вместе с диапазоном дат, и каждый день, на который выпадает повторяющаяся задача, станет отдельной записью — включая даты, которых никто не касался: до вашего запроса отдельными записями они не существуют.

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 }
}

Что важно знать про этот разрез:

  • диапазон обязателен и не может превышать 31 день. Будущие вхождения собираются из правила, поэтому у запроса без границы нет конца;
  • диапазон применяется к date задачи — дню, на который она назначена, — а не к дедлайну. due_date_from / due_date_to продолжают работать как обычные фильтры внутри разреза;
  • разовые задачи, назначенные на дни диапазона, приходят вместе с вхождениями; задача без даты — нет: её нечем поставить в день;
  • отредактированное вхождение приходит один раз — той записью, которой оно на самом деле является;
  • порядок — от начала диапазона, пагинация обычная: next_cursor, пока has_more не станет false. Курсоры разрезов не взаимозаменяемы: курсор одного списка другой отклонит с 400;
  • closed_since с этим разрезом не сочетается (400) — тот фильтр сортирует по моменту выполнения, а двух порядков в одном списке не бывает. Про выполненное спрашивайте отдельно;
  • за один запрос разворачивается не больше 500 повторяющихся задач.

date_from / date_to работают и без view — как обычный фильтр по дню, на который назначена задача.

Узнать следующее вхождение сразу после закрытия текущего

Заголовок раздела «Узнать следующее вхождение сразу после закрытия текущего»

PATCH принимает тот же параметр view. С view=by_task закрытие вхождения возвращается самой повторяющейся задачей, у которой current_occurrence уже указывает на следующее, — дополнительный запрос не нужен:

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_..."}'

Без view в ответе придёт изменённая запись, как и раньше.

Правка «это и последующие вхождения» оставляет две повторяющиеся задачи — часть до изменения и часть после. Так же ведут себя календари, и именно поэтому в проекте могут оказаться две строки, выглядящие как один и тот же стендап.

Что они связаны, показывает series_chain_id:

  • правка свойств (название, исполнитель, описание) оставляет обе половины в одной цепочке — общий series_chain_id, разные series_id;
  • правка времени или правила повторения начинает по-настоящему новую серию, и у новой половины будет свой series_chain_id.

Половина, у которой вхождения закончились, продолжает существовать (её прошлые вхождения — реальная история), но current_occurrence у неё null.

В разрезе by_task фильтр state спрашивает, есть ли работа, а не смотрит на поле статуса:

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

Повторяющаяся задача считается открытой, пока у неё есть открытое вхождение, и закрытой, когда их не осталось, — независимо от того, с какого статуса начинается каждое вхождение. Без view=by_task фильтр работает по собственному статусу записи, как и раньше.

Недавно выполненное — отдельный вопрос с отдельным ответом: выполненные вхождения это самостоятельные записи, поэтому они запрашиваются без view=by_task:

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

Оба фильтра в контексте — в Фильтрации задач.

GET /scheduled-events принимает тот же параметр view с тем же смыслом:

view Одна запись — это
не указан / by_date одно вхождение: поведение по умолчанию, оно не менялось
by_task одно событие; у повторяющегося внутри правило и ближайшее вхождение запрошенного диапазона в current_occurrence
Terminal window
curl "$BASE/scheduled-events?from=2026-09-01T00:00:00Z&to=2026-09-30T00:00:00Z&view=by_task" -H "$AUTH"

Диапазон from / to остаётся обязательным в обоих разрезах. Поля у события те же, что у задачи, — kind (event, recurring_event, occurrence), series_id, series_chain_id, recurrence_summary, current_occurrence, — поэтому один и тот же код читает и то, и другое.

  • Создание повторяющейся задачи или события.
  • Чтение, обновление и удаление как серии, так и отдельного вхождения по его id.
  • Список повторяющихся задач и событий как задач и событий (view=by_task), с ближайшим вхождением внутри.
  • Список вхождений за период — view=by_date у задач и список по умолчанию у событий.
  • Выбор области действия правки через scope — см. ниже.

Правка одного вхождения, последующих или всех

Заголовок раздела «Правка одного вхождения, последующих или всех»

PATCH и DELETE принимают query-параметр scope:

scope На что действует
не указан / only_this только то вхождение, к которому вы обратились — по умолчанию, как и раньше
this_and_following это вхождение и все последующие; серия делится надвое
all все вхождения, включая прошлые
Terminal window
# Переименовать остаток серии, не трогая историю
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": "Стендап (асинхронный)"}'

Для задачи, которая не повторяется, параметр игнорируется.

Обращение к самой повторяющейся задаче требует scope

Заголовок раздела «Обращение к самой повторяющейся задаче требует scope»

Если id, к которому вы обращаетесь в PATCH или DELETE, — это повторяющаяся задача целиком (series_id, а не id вхождения), и scope не передан, запрос отклоняется с 400. Случайно переименовать или удалить всю серию — необратимо, а два идентификатора похожи, поэтому API просит сказать, что вы имели в виду: передайте scope=all либо обратитесь к конкретному вхождению по его собственному id.

scope=all записывает переданные поля по всей серии и не трогает остальное: если кто-то перенёс один вторник или сменил исполнителя на одной дате, это сохранится.

Некоторые поля размножить по серии нельзя, и они отклоняются с 400, а не применяются наполовину:

Поле Почему
assignee_ids, custom_fields внутри лежат идентификаторы, привязанные к конкретной записи; копирование по вхождениям испортило бы записи
tag_ids теги ставятся своим механизмом, со своим поведением для повторений
due_date дедлайн каждого вхождения вычисляется из его собственной даты — общего значения у серии нет

Смена расписания (даты или правила повторения) — это this_and_following, а не all: она делит серию, а не переписывает её прошлое.

DELETE ?scope=all удаляет повторяющуюся задачу вместе со всеми её вхождениями.