# Tasks and schedules

> Create tasks, manage subjects and timetables, search data and change settings.

Source: https://workroad.app/docs/api/tasks-and-schedules

## Tasks

`PATCH /tasks/{taskId}` changes only the fields in the body. A field you leave out keeps its value, and `null` clears `description`, `dueDate`, `subjectId` or `recurrence`. A body without any known field returns 400 `invalid_request`.

Unknown fields in a request body or query string are rejected with a 400 that names the field. Dates in a request body must be strings, such as `2030-01-01` or `2030-01-01T09:00:00Z`. A number, a boolean or a `null` where a date is required returns 400 `invalid_request`. `null` is only valid for `dueDate`, where it means no due date. Clearing `dueDate` also stops any recurrence, and a recurring task cannot lose its due date any other way.

A recurrence repeats a top-level task when you complete it. Set it with `recurrence` on `POST /tasks` or `PATCH /tasks/{taskId}`, and stop it with `"recurrence": null`. It needs a due date. A subtask cannot have a recurrence, and a `PATCH` that gives one to a subtask returns 400 `invalid_request`.

A task's `type` is the kind of work, and must be one of the user's task types from `GET /task-types`, such as `Homework`, `Test` or `Project`, matched without regard to case. The course or module is the subject, never the type. A type the user doesn't have returns 400 `invalid_request` with `details.reason` `unknown-task-type` and their types in `details.taskTypes`; send `createType: true` to add it, as adding a type in the app's task settings does, up to 40 types. An omitted type is `Generic`.

Name the subject with `subjectId`, or with `subjectName`, which must match one of the user's active subjects ignoring case and accents. A name that matches none returns 400 with `details.reason` `unknown-subject` and the user's subjects in `details.subjects`. Send `createSubjectIfMissing: true` to create it, which also needs `subjects:create`. Tasks, study sets and note folders all take `subjectName`.

To record an exam or test, create a task with type `Test`, its day as `dueDate`, the subject, and the topics and start time in the description.

A due date is a day. Send `YYYY-MM-DD`: WorkRoad reads it in the user's time zone (`timeZone` on `GET /me`, UTC when unset) and stores the start of that day, as the app's task form does. A timestamp with `Z` or an offset is accepted too, and the task is due on the day it falls on in that zone. The app shows the day, never a time. Recurrence `endsAt` works the same way.

`POST /tasks/batch` creates up to 50 tasks in one call and answers 200 with one result per task, in order: `created` with the task, or `rejected` with the `status`, `code`, `message` and `details` that `POST /tasks` would have returned. A subject or type the call creates for one task is there for the next. Fix the rejected tasks and send only those again.

`POST /tasks/{taskId}/subtasks` adds a checklist item under a top-level task. A subtask takes its parent's type and subject and cannot have subtasks of its own. A subtask name may be 1 character long at creation, but renaming it with `PATCH /tasks/{taskId}` needs at least 2.

A `subjectId` in a body that is not one of the key's user's subjects returns 400 `invalid_request`, with `details.field` `"subjectId"` and `details.reason` `"unknown-subject"`. This holds on every endpoint that takes a `subjectId`. A 404 always means the resource in the path is missing.

## Subjects

A subject tags the tasks, notes, study sets and schedule elements the key's user owns. A subject has at most 20 teachers, each up to 120 characters. `DELETE /subjects/{subjectId}` unfiles it from all of those, then deletes it. It leaves timetable elements on schedules the key's user doesn't own untouched, even when those schedules reference the same subject.

## Today

`GET /today` answers "what should I study now?" in one call, from the same data the app's dashboard and planning copilot read. `study` has the cards due now by subject and the sets with the most due, today's goal, streak and remaining new cards. `tasks` has overdue tasks and tasks due in the next 7 days, with exams flagged. `lessons` and `studyBlocks` hold today's timetable and planned study time.

Each section needs its own scope and is `null` without it: `study` needs `activity:view` (its `sets` list also needs `sets:view`), `tasks` needs `tasks:view`, and `lessons` and `studyBlocks` need `schedule:view`. `suggestions` lists what to do next, most useful first. Each one has a sentence for the user, the operation that acts on it as `tool`, and its `arguments`, such as `startPracticeSession` with `{ "subjectId": "..." }`. A suggestion only names an operation the key may call.

## Search

`GET /search?q=mitosis` searches the user's notes and their headings, lecture recordings, note files, study sets and terms, tasks, timetables, copilot conversations and practice tests, the same search as the app's command menu. Hits come best match first, 20 a page; pass `nextCursor` back as `cursor` with the same query for the next page. `semantic=true` adds matches by meaning.

A key searches only the record types it can read: `note` and `recording` need `notes:view`, `file` needs `files:view`, `set` and `term` need `sets:view`, `task` needs `tasks:view`, `schedule` needs `schedule:view`, `conversation` needs `copilot:use` and `test` needs `tests:view`. Without `types`, a search covers every type the key can read. Naming a type it can't read returns 403 `forbidden` with the types in `details.types`. Searches share the app's limit of 60 a minute per user.

Each hit's `id` opens with the type's own operation, such as `GET /notes/{noteId}` for a `note`. A `term`'s `parentId` is its study set, and a `recording` or `file`'s is its note.

## Study blocks

A study block is study time planned on the user's calendar, the way the planning copilot plans it. `POST /study-blocks` takes a `title`, a future `startsAt` and an `endsAt` 5 minutes to 8 hours later on the same day in the user's time zone, and optionally the `subjectId`, `taskId` or `setId` it is for. A block can't overlap a lesson, including lessons on connected external calendars, or another block, and can't end after a linked task's due day; those return 409 `conflict` with `details.reason` `planning-conflict` or `planning-after-deadline`. With an external calendar connected, a block can only be planned this week or next, so its lessons can be checked (`planning-calendar-incomplete`).

`GET /study-blocks?from=2026-10-20&to=2026-10-26` lists blocks by day in the user's time zone, up to 62 days at a time. `PATCH` moves or relinks one and checks it again as a whole; `DELETE` removes it.

## Timetables

`POST /schedules/timetable` creates a timetable with its periods and every day's lessons in one call, all or nothing, as the app's setup does. A `defined` timetable has `periods` and one entry per period on each day; a `dynamic` one has none, and each lesson carries `timeStart` and `timeEnd`. A lesson's `subjectName` links it to the user's subject with that name; one that matches none stays as written on the timetable, or becomes a new subject with `createSubjectsIfMissing: true`.

`GET /schedules/{scheduleId}` says what a key can change. `owned` is true for the user's own schedules, and `editable` is true when the lesson grid can be written. Only the owner writes. A schedule shared with the user, or anyone else's, returns 404 on every write.

`PATCH /schedules/{scheduleId}` renames, archives, restores or sets the default, and changes the days the schedule shows. It works on any schedule the user owns, including one read from an integration. Every field in a call applies, or none does. Archiving the default clears it. To make an archived schedule the default, restore it in the same call with `archived: false`.

The grid, meaning the periods and the lessons in them, can only be written on the owner's `DEFINED` schedules kept in WorkRoad. On an external schedule a grid write returns 409 `conflict` with `details.reason` `external-schedule`, and on a `DYNAMIC` one `dynamic-schedule`. Their lessons change in the app or at the provider.

To build a timetable:

1. `POST /schedules` with a `name` and the `structure` of periods. Each period has a `name` and either both `start` and `end` or neither.
2. `POST /schedules/{scheduleId}/elements` with a `day` (0 is Sunday), a `period` index and a `lessonName`, once per lesson. The first lesson lays out an empty period on every weekday. If the user has no default schedule, this one becomes it.
3. Change a lesson with `PATCH /schedules/{scheduleId}/elements/{elementId}` and empty its period with `DELETE`. The element keeps its id with `lessonName: null`, the way the app stores an empty period, and a later `POST` for that period fills the same element. Removing a lesson needs `schedule:delete`, the same way deleting a study set term needs `sets:delete`.

Don't store element ids. Saving the timetable in the app deletes and recreates every element, so an id from an earlier read can return 404. Read `GET /schedules/{scheduleId}` before each lesson write and take the id from `days[].elements[]`.

`PUT /schedules/{scheduleId}/structure` renames and retimes periods at any point. It can change how many periods there are only until the first lesson is added, up to 30; after that it returns 409 with `details.reason` `period-count`. A period's `start` and `end` may be empty strings, which is how the app stores a time still being set up, so a `structure` you read can be sent back unchanged. Adding to a period that doesn't exist returns 409 `no-such-period`, adding to one that has a lesson returns 409 `slot-taken`, and updating an empty one returns 409 `slot-empty`.

Every write returns the schedule as `GET /schedules/{scheduleId}` shows it. A `subjectId` must be one of the user's subject ids; any other returns 400 with `details.field` `subjectId` and `details.reason` `unknown-subject`. `PATCH /schedules/{scheduleId}` with `isDefault: true` on an external schedule first asks the provider whether setup finished. If that check fails it returns 503 before saving anything, and retrying the same body is safe. A lesson reads back with `subjectId` when it points at a subject, or with `subjectLabel` when an older timetable stored free text there; at most one is set. Writing `subjectId` replaces the label. When the provider can't be read for the response after a save, the schedule comes back with `issue: "failed"`.

## Settings

`/settings/profile`, `/settings/preferences`, `/settings/notifications`, `/settings/review`, `/settings/tasks` and `/settings/dashboard` read with `GET` and change with `PATCH` (`PUT` for the dashboard), the same as the Settings pages. A `PATCH` changes only the fields you send. Each writes through the code the page saves with, so the limits and errors match the app's.

- Changing `timeZone` moves every task's due date so it keeps its date and time on the clock. A zone the server doesn't know returns 400, and a change made elsewhere during the request returns 409.
- `PATCH /settings/notifications/{kind}` changes one kind's channels and timing. A timing field the kind doesn't use, or a value off its list, returns 400 with `details.field` and the allowed values in `details.reason`.
- `subjectOverrides` on `PATCH /settings/review` replaces the whole list, and every `subjectId` must be the user's.
- `PUT /settings/dashboard` takes the cards to show, in order. Send the `updatedAt` you read as `baseUpdatedAt` to get 409 instead of overwriting a layout saved elsewhere.

`GET /calendar-feeds` lists feed metadata without returning secret subscription URLs. `DELETE /calendar-feeds/{feedId}` revokes a feed. Create feeds or rotate their URLs in [Settings → Calendar](https://workroad.app/user/calendar). Anyone holding a subscription URL can read its calendar data, even after the API key used to list the feed is revoked.

A Copilot conversation in which a turn read or proposed the user's settings is only readable with `settings:view`: without it, `GET /copilot/conversations` leaves it out, its messages, its replies' jobs and sending into it return 403 with `details.reason` `settings-in-conversation`, and search skips it.

The API can't change the password, the email address, two-factor, passkeys, sign-in methods, sessions, API keys, connected apps, the plan or payment, and can't delete the account. The theme and the AI model choice are kept in the browser.
