Recurring & repeating tasks
Bordio has two different ways a task can repeat. They are mutually exclusive — pick one:
- On a schedule (
recurrence) — the task/event repeats on fixed dates, e.g. every Monday. Uses an iCalendarRRULE. Works for tasks and events. Covered first, below. - After completion (
repeat_on_completion) — a fresh copy is created only when you close the task, with dates shifted forward. Good for “do this again N days after I finish it”. Tasks only. See Repeat after completion.
On a schedule (recurrence)
Section titled “On a schedule (recurrence)”A task or event can repeat on a schedule — a daily task, a weekly standup, a monthly review. You create a repeating series by adding a recurrence object to the normal create request. The result is a series (one template you address by its id), not a pile of copies.
Recurrence uses the iCalendar RRULE format (RFC 5545) — the same rule syntax as Google Calendar and CalDAV. If you have ever built a recurrence with a calendar library, you already know it.
Creating a recurring event
Section titled “Creating a recurring event”Add recurrence to POST /scheduled-events. The series is anchored at start_at.
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" } }'This creates a standup every Monday, Wednesday and Friday from 06 Jul 2026 until the end of the year.
Creating a recurring task
Section titled “Creating a recurring task”Add recurrence to POST /tasks. The series is anchored by the task’s date (the day the task is scheduled) — you must include date, otherwise the request is rejected with 422.
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" } }'The recurrence object
Section titled “The recurrence object”| Field | Required | Description |
|---|---|---|
rule |
yes | A bare iCalendar RRULE string, e.g. RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR. |
until |
no | ISO-8601 date/datetime — the inclusive end of the series. |
Writing the rule
Section titled “Writing the rule”The rule is a standard RRULE line. The most common parts:
FREQ=— how often:DAILY,WEEKLY,MONTHLYorYEARLY.INTERVAL=— every N periods (e.g.INTERVAL=2withFREQ=WEEKLY= every two weeks). Defaults to1.BYDAY=— days of the week:MO,TU,WE,TH,FR,SA,SU(use withFREQ=WEEKLY).BYMONTHDAY=— day of the month, e.g.BYMONTHDAY=1(use withFREQ=MONTHLY).
Examples:
| Goal | rule |
|---|---|
| Every day | RRULE:FREQ=DAILY |
| Every weekday | RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR |
| Every other Monday | RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=MO |
| First of every month | RRULE:FREQ=MONTHLY;BYMONTHDAY=1 |
| Every year | RRULE:FREQ=YEARLY |
Rules and limits
Section titled “Rules and limits”To keep series valid and safe, the API is strict about the rule:
FREQis required and must be one ofDAILY,WEEKLY,MONTHLY,YEARLY. Sub-daily frequencies (HOURLY,MINUTELY,SECONDLY) are rejected.- Do not include
DTSTART. The start is derived from the resource (start_atfor events,datefor tasks). ADTSTARTin theruleis rejected. - Do not use
COUNT. Bound the series with theuntilfield instead. - The series cannot start more than 730 days in the past (
start_atfor events,datefor tasks). Older history is imported as one-off records or as a series anchored at a recent date; existing series are not affected. INTERVAL, if present, must be a positive integer (≤ 1000).- The
rulemust be a singleRRULE:line (noRDATE/EXDATE, no line breaks).
Anything else returns 422 validation_error with code invalid_recurrence_rule.
When does it end?
Section titled “When does it end?”Use the until field for the end date. If you omit until, the series is open-ended and repeats indefinitely.
Reading a series back
Section titled “Reading a series back”Recurring resources include a recurrence object in responses; single (non-recurring) ones return 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" } }}The returned rule is normalized to the RRULE: line — the internal DTSTART is not exposed.
Repeat after completion (tasks only)
Section titled “Repeat after completion (tasks only)”Instead of a fixed schedule, a task can spawn a new copy of itself when you complete it (move it to a closed status). This is a different mechanism from recurrence above — there is no RRULE, and nothing is created until the task is actually closed. Add repeat_on_completion to POST /tasks:
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" } }'When this task is closed, Bordio creates a fresh copy: same title, project, assignees, priority and subtasks, with its start date set 30 days after the completion date and its due date 2 days after that. The copy repeats the same way, until until passes.
| Field | Required | Description |
|---|---|---|
start_date_shift_days |
no | Days after completion the next copy starts. 0 = same day. Omit or pass null to leave the copy unscheduled — it lands in the waiting list. |
due_date_shift_days |
no | Days after the copy’s start that its due date falls. Omit for no due date. Ignored when the copy has no start date. |
until |
no | Stop repeating after this date (ISO-8601). Omit to repeat indefinitely. |
initial_task_status_id |
no | Status each new copy starts in (task_status_...). Defaults to the task’s status — set it so the copy begins as e.g. “To do” instead of the closed status that triggered it. |
Notes:
- Only when closed. The copy is created when the task moves to a status whose state is closed — not on every edit.
- Fields are inherited. Title, description, project, assignees, reporters, priority, subtasks and the time estimate carry over to each copy automatically; you only specify the date shifts.
- Waiting-list repeats. Without
start_date_shift_daysthe copy has no start date, so it waits in the waiting list until someone schedules it. Because the due date is computed from the start date,due_date_shift_dayshas no effect in this mode. - Mutually exclusive with
recurrence. Sending both on the same task returns422.
Responses include a repeat_on_completion object (or 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" } }}Series vs. occurrences
Section titled “Series vs. occurrences”- The series (the repeating task itself) has a plain id —
task_<id>/event_<id>— the id you get back from create. Reading, updating or deleting it applies to the whole series. - An individual occurrence has its own id that carries the occurrence date, e.g.
task_<id>_20260113T090000Z(see Identifiers). Read it back from the API and use it verbatim to address that single occurrence — never build it yourself.
Listing repeating tasks
Section titled “Listing repeating tasks”GET /tasks answers two different questions, and which one you get depends on the view parameter.
Without view the list contains your one-off tasks plus the individual occurrences that already exist in the system — the ones somebody has edited, completed or rescheduled. The repeating task itself is not in that list, and one series can appear as several rows. This is the historical behaviour and it does not change.
With view=by_task the list contains tasks: a repeating task is one row, carrying its rule and the occurrence to work on next. Its occurrences are not listed separately.
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": "Team 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": "Team standup", "task_status_id": "task_status_6594a1b2c3d4e5f6a7b8c9d2" } } ]}These fields are present on every task the API returns, in any mode — they were added alongside the existing ones, so a client that does not read them sees no change:
| Field | Meaning |
|---|---|
kind |
task (one-off), recurring_task (repeats on a rule), occurrence (one dated occurrence) |
series_id |
The repeating task this record belongs to. A repeating task carries its own id here; a one-off task has null |
series_chain_id |
Groups repeating tasks that came from one original series — see Splitting a series |
recurrence_summary |
The same rule in words, e.g. every weekday |
current_occurrence |
The occurrence to work on next, in full. null once a repeating task has no open occurrences left |
current_occurrence is the earliest open occurrence, including overdue ones: an abandoned daily task shows the oldest thing you still owe, not tomorrow’s copy. It is the full task object, because an occurrence that somebody edited can differ from the series — its own title, assignees, status or due date — and you should not have to fetch it separately to find that out.
New integrations should always pass view explicitly.
Listing occurrences day by day (view=by_date)
Section titled “Listing occurrences day by day (view=by_date)”The third question a listing can answer is “what happens between these dates”. Pass
view=by_date with a date range, and every day the repeating task falls on becomes its own
record — including the dates nobody has touched yet, which do not exist as separate records until
you ask for them:
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 }}What to know about this view:
- the range is required and cannot exceed 31 days. Future occurrences are generated from the rule, so an open-ended request has no end;
- the range applies to the task’s
date— the day it is scheduled for — not to its due date.due_date_from/due_date_tostill work as ordinary filters inside the view; - one-off tasks scheduled in the range are listed alongside occurrences; a task with no date is not (there is no day to put it on);
- an occurrence somebody edited appears once, as the record it actually is;
- results are ordered from the start of the range, and pagination works as usual —
next_cursoruntilhas_moreisfalse. Cursors are not interchangeable between views: a cursor from one listing is rejected with400by the other; closed_sincecannot be combined with it (400) — that filter orders by completion time, and a list cannot have two orders. Ask for completed work separately;- at most 500 repeating tasks are expanded in one request.
date_from / date_to also work without view, as a plain filter on the day a task is
scheduled for.
Knowing the next occurrence right after you close one
Section titled “Knowing the next occurrence right after you close one”PATCH accepts the same view parameter. With view=by_task, closing an occurrence comes back as the repeating task itself, with current_occurrence already pointing at the next one — no follow-up request needed:
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_..."}'Without view the response is the record you changed, as before.
Splitting a series
Section titled “Splitting a series”Editing “this and the following occurrences” leaves you with two repeating tasks — the part before the change and the part after it. This is how calendar apps behave, and it is why a project can contain two rows that look like the same standup.
series_chain_id tells you they belong together:
- editing properties (title, assignee, description) keeps both halves in the same chain — same
series_chain_id, differentseries_id; - editing the time or the repeat rule starts a genuinely new series, and the new half gets its own
series_chain_id.
A half that has run out of occurrences keeps existing (its past occurrences are real history), but its current_occurrence is null.
Finding what to work on
Section titled “Finding what to work on”In the by_task view, state asks whether there is work to do rather than looking at a status field:
curl "$BASE/tasks?view=by_task&state=open" -H "$AUTH"A repeating task counts as open while it still has an open occurrence, and as closed once none are left — regardless of the status each occurrence starts in. Without view=by_task the filter works on the record’s own status, as it always has.
Recently completed work is a separate question with a separate answer — completed occurrences are individual records, so they are listed without view=by_task:
curl "$BASE/tasks?closed_since=2026-07-28" -H "$AUTH"See Filtering tasks for both filters in context.
Repeating events in a listing
Section titled “Repeating events in a listing”GET /scheduled-events takes the same view parameter, with the same meaning:
view |
One record is |
|---|---|
omitted / by_date |
one occurrence — the default, unchanged behaviour |
by_task |
one event; a repeating one carries its rule and, in current_occurrence, the next occurrence inside the requested range |
curl "$BASE/scheduled-events?from=2026-09-01T00:00:00Z&to=2026-09-30T00:00:00Z&view=by_task" -H "$AUTH"The from / to range stays required in both views. Events carry the same fields as tasks —
kind (event, recurring_event, occurrence), series_id, series_chain_id,
recurrence_summary, current_occurrence — so one piece of client code can read both.
What is supported today
Section titled “What is supported today”- Create a recurring task or event.
- Read, update and delete both a series and an individual occurrence by its id.
- List repeating tasks and events as tasks/events (
view=by_task), with the next occurrence included. - List occurrences over a date range —
view=by_datefor tasks, and the default listing for events. - Choose what an edit affects with
scope— see below.
Editing one occurrence, the rest, or all of them
Section titled “Editing one occurrence, the rest, or all of them”PATCH and DELETE take a scope query parameter:
scope |
What it affects |
|---|---|
omitted / only_this |
just the occurrence you addressed — the default, and the previous behaviour |
this_and_following |
that occurrence and every later one; this splits the repeating task in two |
all |
every occurrence, past ones included |
# Move the rest of the series to a new assignee, leaving history alonecurl -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": "Team standup (async)"}'For a task that does not repeat the parameter is ignored.
Addressing the repeating task itself requires scope
Section titled “Addressing the repeating task itself requires scope”If the id you PATCH or DELETE is the repeating task (the series_id, not an occurrence id) and you did not pass scope, the request is rejected with 400. Renaming or deleting an entire series by accident is not something you can undo, and the two ids look alike — so the API asks you to say what you meant: pass scope=all, or address a single occurrence by its own id.
What scope=all can and cannot change
Section titled “What scope=all can and cannot change”scope=all writes the fields you send across the whole series and leaves everything else alone: if someone moved one Tuesday or reassigned a single date, that stays.
Some fields cannot be spread across a series and are rejected with 400 rather than applied halfway:
| Field | Why |
|---|---|
assignee_ids, custom_fields |
they contain per-record identifiers, so copying them across occurrences would corrupt the records |
tag_ids |
tags are applied through their own mechanism, with their own recurrence behaviour |
due_date |
each occurrence derives its deadline from its own date — there is no series-wide value |
Changing the schedule (the date or the repeat rule) is this_and_following, not all: it splits the series rather than rewriting its past.
DELETE ?scope=all removes the repeating task together with every occurrence of it.