Skip to content
WorkRoad home
REST API

Tasks and schedules

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

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.

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