# Tasks

> Create tasks one at a time or in batches, set due days and recurrence, and add subtasks.

Source: https://workroad.app/docs/api/tasks

## Creating and changing tasks

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

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

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

## Types and subjects

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

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.

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.

## Due dates

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.

## Repeating tasks

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

## List tasks

`GET https://workroad.app/api/v1/tasks`

Newest first, subtasks included. Filter with `completed`, `type`, `subjectId`, `parentId` and the due range `dueFrom` (inclusive) to `dueTo` (exclusive). A due range leaves out tasks without a due date.

- MCP tool: `listTasks` on `https://workroad.app/api/mcp`
- Required scopes: tasks:view
- Test mode: `GET https://workroad.app/api/test/v1/tasks` checks the request and returns the example response without changing anything.

### Query parameters

- `limit`: integer, 1 to 100, default `25`. Page size, 1-100.
- `cursor`: string or null, up to 512 characters. Opaque. Pass `nextCursor` from the previous page unchanged.
- `completed`: one of `"true"`, `"false"`. Filter by completion state. Omit for both.
- `dueFrom`: string (date-time). Only tasks due at or after this ISO-8601 time.
- `dueTo`: string (date-time). Only tasks due before this ISO-8601 time.
- `type`: string, 1-32 characters. Only tasks of this type.
- `subjectId`: string (uuid). Only tasks filed under this subject.
- `parentId`: `"none"` or string (uuid). `none` for top-level tasks only, or a task id for that task's subtasks.
  - Or `"none"`
  - Or string (uuid)

### Response

`200` OK

- `data`: array of objects, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `description`: string or null, required
  - `type`: string, required. Free-form task type from the user's task settings.
  - `subjectId`: string or null, required. Subject id, when set.
  - `dueDate`: string (date-time) or null, required. Due date, when set.
  - `completed`: boolean, required. Public name for the `finished` column.
  - `priority`: one of `"none"`, `"low"`, `"medium"`, `"high"`, required. How urgent the task is: `none`, `low`, `medium` or `high`, from least to most urgent. `none` when unset.
  - `parentId`: string or null, required. The parent task's id for a subtask, `null` for a top-level task.
  - `createdAt`: string (date-time), required. Creation timestamp.
  - `updatedAt`: string (date-time), required. Last update timestamp.
- `nextCursor`: string or null, required. Opaque. Pass as `?cursor=` unchanged to fetch the next page. `null` on the last page.

#### Example

```json
{
  "data": [
    {
      "id": "4d3c2b1a-0f9e-4d8c-9b7a-6f5e4d3c2b1a",
      "name": "Finish lab report",
      "description": "Include the error analysis.",
      "type": "Generic",
      "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
      "dueDate": "2026-02-10T17:00:00.000Z",
      "completed": false,
      "priority": "high",
      "parentId": null,
      "createdAt": "2026-01-15T09:30:00.000Z",
      "updatedAt": "2026-02-01T12:00:00.000Z"
    }
  ],
  "nextCursor": null
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Get a task

`GET https://workroad.app/api/v1/tasks/{taskId}`

Returns one task or subtask with its subtask count and recurrence rule. List a task's subtasks with `GET /tasks?parentId=<taskId>`.

- MCP tool: `getTask` on `https://workroad.app/api/mcp`
- Required scopes: tasks:view
- Test mode: `GET https://workroad.app/api/test/v1/tasks/{taskId}` checks the request and returns the example response without changing anything.

### Path parameters

- `taskId`: string (uuid), required. Task id, scoped to the key's user.

### Response

`200` OK

- `data`: object, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `description`: string or null, required
  - `type`: string, required. Free-form task type from the user's task settings.
  - `subjectId`: string or null, required. Subject id, when set.
  - `dueDate`: string (date-time) or null, required. Due date, when set.
  - `completed`: boolean, required. Public name for the `finished` column.
  - `priority`: one of `"none"`, `"low"`, `"medium"`, `"high"`, required. How urgent the task is: `none`, `low`, `medium` or `high`, from least to most urgent. `none` when unset.
  - `parentId`: string or null, required. The parent task's id for a subtask, `null` for a top-level task.
  - `createdAt`: string (date-time), required. Creation timestamp.
  - `updatedAt`: string (date-time), required. Last update timestamp.
  - `subtaskCount`: integer, required
  - `recurrence`: object or null, required. How the task repeats, or `null` when it does not.
    - `unit`: one of `"day"`, `"week"`, `"month"`, required
    - `interval`: integer, required
    - `endsAt`: string (date-time) or null, required. No occurrence is created after this time, when set.

#### Example

```json
{
  "data": {
    "id": "4d3c2b1a-0f9e-4d8c-9b7a-6f5e4d3c2b1a",
    "name": "Finish lab report",
    "description": "Include the error analysis.",
    "type": "Generic",
    "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
    "dueDate": "2026-02-10T17:00:00.000Z",
    "completed": false,
    "priority": "high",
    "parentId": null,
    "createdAt": "2026-01-15T09:30:00.000Z",
    "updatedAt": "2026-02-01T12:00:00.000Z",
    "subtaskCount": 2,
    "recurrence": {
      "unit": "week",
      "interval": 1,
      "endsAt": null
    }
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No task with that id exists for this key's user.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Create a task

`POST https://workroad.app/api/v1/tasks`

Creates a task for the key's user. `type` is the kind of work and must be one of the user's task types (`GET /task-types`); the course or module is the subject, sent as `subjectId` or `subjectName`. List the subjects and types first, and create a missing subject (or ask the user) rather than putting its name in another field. An exam or in-class test is type `Test`, with its day as `dueDate` and the topics and start time in the description. To add several tasks, send them in one `POST /tasks/batch`.

Use this to save content you wrote yourself. It uses none of the user's WorkRoad AI uses.

- MCP tool: `createTask` on `https://workroad.app/api/mcp`
- Required scopes: tasks:create
- Test mode: `POST https://workroad.app/api/test/v1/tasks` checks the request and returns the example response without changing anything.

### Headers

- `Idempotency-Key`: string, optional, 1-255 visible ASCII characters. Makes the POST safe to retry: a retry with the same key and body replays the first response, and the same key with a different body returns 409 `idempotency_conflict`. Kept for 24 hours. Over MCP it is the `idempotencyKey` argument.

### Request body

JSON.

- `name`: string, required, 2-200 characters
- `description`: string, up to 512 characters. Plain text, up to 512 characters: the topics an exam covers, the time it starts, what to hand in.
- `dueDate`: string (date) or string (date-time) or null. The day the task is due. `null` or omitted for no due date. Send the day as `YYYY-MM-DD`: it is read in the user's time zone (`timeZone` on `GET /me`) and stored as 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 time zone. The app shows a due date as a day, never a time, so put a time such as an exam's start in the description.
  - Or string (date)
  - Or string (date-time)
- `type`: string, 2-32 characters. The kind of work, one of the user's task types from `GET /task-types` (such as `Homework`, `Test`, `Project`, `Study` or `Read`), matched without regard to case. Never a course or module name: that is the subject, so send it as `subjectId` or `subjectName`. An exam or in-class test is `Test`. A type the user doesn't have is refused with 400 `unknown-task-type`, whose `details.taskTypes` lists theirs, unless `createType` is true. Defaults to `Generic` when omitted.
- `createType`: boolean. Adds `type` to the user's task types when they don't have it, as adding one in the app's task settings does. Only send it when the user asked for a new kind of task, never for a course name.
- `subjectId`: string (uuid). Subject id owned by the user.
- `subjectName`: string, 1-64 characters. The subject (the course or module, such as `Software Engineering`) by name instead of by id. It must match one of the user's active subjects, ignoring case and accents. A name that matches none is refused with 400 `unknown-subject`, whose `details.subjects` lists the user's subjects, unless `createSubjectIfMissing` is true. Send `subjectId` or `subjectName`, not both.
- `createSubjectIfMissing`: boolean. With `subjectName`, creates the subject when the user has none by that name. Needs the `subjects:create` scope. Only send it when the user named a course they haven't added yet.
- `recurrence`: object. Repeats the task. Completing it creates the next occurrence `interval` units after the due date. Needs a due date.
  - `unit`: one of `"day"`, `"week"`, `"month"`, required
  - `interval`: integer, required, 1 to 30
  - `endsAt`: string (date) or string (date-time) or null
    - Or string (date)
    - Or string (date-time)
- `priority`: one of `"none"`, `"low"`, `"medium"`, `"high"`. How urgent the task is: `none`, `low`, `medium` or `high`, from least to most urgent. Defaults to `none`. Only send it when the user said how urgent it is.

### Response

`201` Created

- `data`: object, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `description`: string or null, required
  - `type`: string, required. Free-form task type from the user's task settings.
  - `subjectId`: string or null, required. Subject id, when set.
  - `dueDate`: string (date-time) or null, required. Due date, when set.
  - `completed`: boolean, required. Public name for the `finished` column.
  - `priority`: one of `"none"`, `"low"`, `"medium"`, `"high"`, required. How urgent the task is: `none`, `low`, `medium` or `high`, from least to most urgent. `none` when unset.
  - `parentId`: string or null, required. The parent task's id for a subtask, `null` for a top-level task.
  - `createdAt`: string (date-time), required. Creation timestamp.
  - `updatedAt`: string (date-time), required. Last update timestamp.

#### Example

```json
{
  "data": {
    "id": "4d3c2b1a-0f9e-4d8c-9b7a-6f5e4d3c2b1a",
    "name": "Finish lab report",
    "description": "Include the error analysis.",
    "type": "Generic",
    "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
    "dueDate": "2026-02-10T17:00:00.000Z",
    "completed": false,
    "priority": "high",
    "parentId": null,
    "createdAt": "2026-01-15T09:30:00.000Z",
    "updatedAt": "2026-02-01T12:00:00.000Z"
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: `invalid_request` for an invalid body or a recurrence without a due date. `details.reason` `unknown-task-type` lists the user's types in `details.taskTypes`, and `unknown-subject` lists their subjects in `details.subjects`; pick one, or send `createType` or `createSubjectIfMissing`. `too-many-task-types` when `createType` would pass the cap.
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: `createSubjectIfMissing` was sent and the key lacks `subjects:create` (`details.missingScopes`).
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `409`: An identical task already exists. The Idempotency-Key was reused with a different request body, or a request with the same key is still in progress.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Create several tasks

`POST https://workroad.app/api/v1/tasks/batch`

Creates up to 50 tasks in one call, each with the fields `POST /tasks` takes, and answers 200 with a result per task in the order sent: `created` with the task, or `rejected` with the error `POST /tasks` would have returned for it. Each task is created or refused on its own, so fix the rejected ones and send only those again. A subject or task type the first task creates is there for the ones after it. `type` is the kind of work and must be one of the user's task types (`GET /task-types`); the course or module is the subject, sent as `subjectId` or `subjectName`. List the subjects and types first, and create a missing subject (or ask the user) rather than putting its name in another field. An exam or in-class test is type `Test`, with its day as `dueDate` and the topics and start time in the description.

Use this to save content you wrote yourself. It uses none of the user's WorkRoad AI uses.

- MCP tool: `createTasks` on `https://workroad.app/api/mcp`
- Required scopes: tasks:create
- Test mode: `POST https://workroad.app/api/test/v1/tasks/batch` checks the request and returns the example response without changing anything.
- Next: send the `rejected` tasks again after fixing what each `error` says.

### Headers

- `Idempotency-Key`: string, optional, 1-255 visible ASCII characters. Makes the POST safe to retry: a retry with the same key and body replays the first response, and the same key with a different body returns 409 `idempotency_conflict`. Kept for 24 hours. Over MCP it is the `idempotencyKey` argument.

### Request body

JSON.

- `tasks`: array of objects, required, 1-50 items. 1-50 tasks, each with the fields `POST /tasks` takes. Each is created or refused on its own.
  - `name`: string, required, 2-200 characters
  - `description`: string, up to 512 characters. Plain text, up to 512 characters: the topics an exam covers, the time it starts, what to hand in.
  - `dueDate`: string (date) or string (date-time) or null. The day the task is due. `null` or omitted for no due date. Send the day as `YYYY-MM-DD`: it is read in the user's time zone (`timeZone` on `GET /me`) and stored as 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 time zone. The app shows a due date as a day, never a time, so put a time such as an exam's start in the description.
    - Or string (date)
    - Or string (date-time)
  - `type`: string, 2-32 characters. The kind of work, one of the user's task types from `GET /task-types` (such as `Homework`, `Test`, `Project`, `Study` or `Read`), matched without regard to case. Never a course or module name: that is the subject, so send it as `subjectId` or `subjectName`. An exam or in-class test is `Test`. A type the user doesn't have is refused with 400 `unknown-task-type`, whose `details.taskTypes` lists theirs, unless `createType` is true. Defaults to `Generic` when omitted.
  - `createType`: boolean. Adds `type` to the user's task types when they don't have it, as adding one in the app's task settings does. Only send it when the user asked for a new kind of task, never for a course name.
  - `subjectId`: string (uuid). Subject id owned by the user.
  - `subjectName`: string, 1-64 characters. The subject (the course or module, such as `Software Engineering`) by name instead of by id. It must match one of the user's active subjects, ignoring case and accents. A name that matches none is refused with 400 `unknown-subject`, whose `details.subjects` lists the user's subjects, unless `createSubjectIfMissing` is true. Send `subjectId` or `subjectName`, not both.
  - `createSubjectIfMissing`: boolean. With `subjectName`, creates the subject when the user has none by that name. Needs the `subjects:create` scope. Only send it when the user named a course they haven't added yet.
  - `recurrence`: object. Repeats the task. Completing it creates the next occurrence `interval` units after the due date. Needs a due date.
    - `unit`: one of `"day"`, `"week"`, `"month"`, required
    - `interval`: integer, required, 1 to 30
    - `endsAt`: string (date) or string (date-time) or null
      - Or string (date)
      - Or string (date-time)
  - `priority`: one of `"none"`, `"low"`, `"medium"`, `"high"`. How urgent the task is: `none`, `low`, `medium` or `high`, from least to most urgent. Defaults to `none`. Only send it when the user said how urgent it is.

### Response

`200` OK

- `data`: object, required
  - `createdCount`: integer, required. How many tasks were created.
  - `rejectedCount`: integer, required. How many tasks were refused.
  - `results`: array of these objects, required. One result per task, in the order sent.
    - One of `status`: `"created"`
      - `index`: integer, required. The task's position in `tasks`.
      - `status`: `"created"`, required
      - `task`: object, required
        - `id`: string (uuid), required
        - `name`: string, required
        - `description`: string or null, required
        - `type`: string, required. Free-form task type from the user's task settings.
        - `subjectId`: string or null, required. Subject id, when set.
        - `dueDate`: string (date-time) or null, required. Due date, when set.
        - `completed`: boolean, required. Public name for the `finished` column.
        - `priority`: one of `"none"`, `"low"`, `"medium"`, `"high"`, required. How urgent the task is: `none`, `low`, `medium` or `high`, from least to most urgent. `none` when unset.
        - `parentId`: string or null, required. The parent task's id for a subtask, `null` for a top-level task.
        - `createdAt`: string (date-time), required. Creation timestamp.
        - `updatedAt`: string (date-time), required. Last update timestamp.
    - Or `status`: `"rejected"`
      - `index`: integer, required. The task's position in `tasks`.
      - `status`: `"rejected"`, required
      - `error`: object, required
        - `status`: integer, required. The HTTP status the single-item call answers.
        - `code`: string, required. The error code, as in the error envelope.
        - `message`: string, required
        - `details`: any value. What to fix, such as `reason` and the user's `taskTypes` or `subjects`.

#### Example

```json
{
  "data": {
    "createdCount": 1,
    "rejectedCount": 1,
    "results": [
      {
        "index": 0,
        "status": "created",
        "task": {
          "id": "4d3c2b1a-0f9e-4d8c-9b7a-6f5e4d3c2b1a",
          "name": "Finish lab report",
          "description": "Include the error analysis.",
          "type": "Generic",
          "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
          "dueDate": "2026-02-10T17:00:00.000Z",
          "completed": false,
          "priority": "high",
          "parentId": null,
          "createdAt": "2026-01-15T09:30:00.000Z",
          "updatedAt": "2026-02-01T12:00:00.000Z"
        }
      },
      {
        "index": 1,
        "status": "rejected",
        "error": {
          "status": 400,
          "code": "invalid_request",
          "message": "\"Software Engineering\" is not one of the user's task types: \"Generic\", \"Homework\", \"Test\". A task type is the kind of work; a course or module is the subject, so send it as subjectId or subjectName. An exam or test is \"Test\". To add a new type, send createType: true.",
          "details": {
            "field": "type",
            "reason": "unknown-task-type",
            "taskTypes": [
              "Generic",
              "Homework",
              "Test"
            ]
          }
        }
      }
    ]
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: The body is invalid as a whole.
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `409`: The Idempotency-Key was reused with a different request body, or a request with the same key is still in progress.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Update a task

`PATCH https://workroad.app/api/v1/tasks/{taskId}`

Changes only the fields you send; `null` clears a nullable field. `completed` is applied first, and completing a recurring task creates its next occurrence from the task as it was before the other changes. Everything in one call succeeds or fails together.

- MCP tool: `updateTask` on `https://workroad.app/api/mcp`
- Required scopes: tasks:update
- Test mode: `PATCH https://workroad.app/api/test/v1/tasks/{taskId}` checks the request and returns the example response without changing anything.

### Path parameters

- `taskId`: string (uuid), required. Task id, scoped to the key's user.

### Request body

JSON.

- `completed`: boolean. Completes or reopens the task. Completing a recurring task creates its next occurrence.
- `name`: string, 2-200 characters
- `description`: string or null, up to 512 characters. Plain text. `null` or an empty string clears it.
- `dueDate`: string (date) or string (date-time) or null. The day the task is due. `null` clears it and stops any recurrence. Send the day as `YYYY-MM-DD`: it is read in the user's time zone (`timeZone` on `GET /me`) and stored as 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 time zone. The app shows a due date as a day, never a time, so put a time such as an exam's start in the description.
  - Or string (date)
  - Or string (date-time)
- `type`: string, 2-32 characters. The kind of work, one of the user's task types from `GET /task-types` (such as `Homework`, `Test`, `Project`, `Study` or `Read`), matched without regard to case. Never a course or module name: that is the subject, so send it as `subjectId` or `subjectName`. An exam or in-class test is `Test`. A type the user doesn't have is refused with 400 `unknown-task-type`, whose `details.taskTypes` lists theirs, unless `createType` is true.
- `createType`: boolean. Adds `type` to the user's task types when they don't have it, as adding one in the app's task settings does. Only send it when the user asked for a new kind of task, never for a course name.
- `recurrence`: object or null. Repeats the task. `null` stops it. Needs a due date and only works on top-level tasks.
  - `unit`: one of `"day"`, `"week"`, `"month"`, required
  - `interval`: integer, required, 1 to 30
  - `endsAt`: string (date) or string (date-time) or null
    - Or string (date)
    - Or string (date-time)
- `subjectId`: string (uuid) or null. Subject id owned by the user. `null` unfiles the task.
- `subjectName`: string, 1-64 characters. The subject (the course or module, such as `Software Engineering`) by name instead of by id. It must match one of the user's active subjects, ignoring case and accents. A name that matches none is refused with 400 `unknown-subject`, whose `details.subjects` lists the user's subjects, unless `createSubjectIfMissing` is true. Send `subjectId` or `subjectName`, not both.
- `createSubjectIfMissing`: boolean. With `subjectName`, creates the subject when the user has none by that name. Needs the `subjects:create` scope. Only send it when the user named a course they haven't added yet.
- `priority`: one of `"none"`, `"low"`, `"medium"`, `"high"`. How urgent the task is: `none`, `low`, `medium` or `high`, from least to most urgent. `none` clears it.

### Response

`200` OK

- `data`: object, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `description`: string or null, required
  - `type`: string, required. Free-form task type from the user's task settings.
  - `subjectId`: string or null, required. Subject id, when set.
  - `dueDate`: string (date-time) or null, required. Due date, when set.
  - `completed`: boolean, required. Public name for the `finished` column.
  - `priority`: one of `"none"`, `"low"`, `"medium"`, `"high"`, required. How urgent the task is: `none`, `low`, `medium` or `high`, from least to most urgent. `none` when unset.
  - `parentId`: string or null, required. The parent task's id for a subtask, `null` for a top-level task.
  - `createdAt`: string (date-time), required. Creation timestamp.
  - `updatedAt`: string (date-time), required. Last update timestamp.

#### Example

```json
{
  "data": {
    "id": "4d3c2b1a-0f9e-4d8c-9b7a-6f5e4d3c2b1a",
    "name": "Finish lab report",
    "description": "Include the error analysis.",
    "type": "Generic",
    "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
    "dueDate": "2026-02-10T17:00:00.000Z",
    "completed": false,
    "priority": "high",
    "parentId": null,
    "createdAt": "2026-01-15T09:30:00.000Z",
    "updatedAt": "2026-02-01T12:00:00.000Z"
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid body, no field to update, a recurrence was set on a task with no due date, or a subtask was given a recurrence. `details.reason` `unknown-task-type` and `unknown-subject` list the user's types and subjects, as on `POST /tasks`.
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No task with that id exists for this key's user.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Move a task to the trash

`DELETE https://workroad.app/api/v1/tasks/{taskId}`

Moves the task and its subtasks to the trash. A later occurrence of a recurring task is kept. `POST /tasks/{taskId}/restore` brings the task back with its subtasks. The trash is emptied after 30 days.

- MCP tool: `deleteTask` on `https://workroad.app/api/mcp`
- Required scopes: tasks:delete
- Test mode: `DELETE https://workroad.app/api/test/v1/tasks/{taskId}` checks the request and returns the example response without changing anything.
- Next: `restoreTask` brings it back from the trash.

### Path parameters

- `taskId`: string (uuid), required. Task id, scoped to the key's user.

### Response

`200` OK

- `data`: object, required
  - `id`: string (uuid), required
  - `deleted`: `true`, required

#### Example

```json
{
  "data": {
    "id": "4d3c2b1a-0f9e-4d8c-9b7a-6f5e4d3c2b1a",
    "deleted": true
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No task with that id exists for this key's user.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Restore a task from the trash

`POST https://workroad.app/api/v1/tasks/{taskId}/restore`

Brings a trashed task back with the subtasks that went to the trash with it, and returns it. Restoring a subtask whose task is in the trash restores that task too. Needs `tasks:delete`, the scope that trashed it.

- MCP tool: `restoreTask` on `https://workroad.app/api/mcp`
- Required scopes: tasks:delete
- Test mode: `POST https://workroad.app/api/test/v1/tasks/{taskId}/restore` checks the request and returns the example response without changing anything.

### Path parameters

- `taskId`: string (uuid), required. Task id, scoped to the key's user.

### Headers

- `Idempotency-Key`: string, optional, 1-255 visible ASCII characters. Makes the POST safe to retry: a retry with the same key and body replays the first response, and the same key with a different body returns 409 `idempotency_conflict`. Kept for 24 hours. Over MCP it is the `idempotencyKey` argument.

### Response

`200` OK

- `data`: object, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `description`: string or null, required
  - `type`: string, required. Free-form task type from the user's task settings.
  - `subjectId`: string or null, required. Subject id, when set.
  - `dueDate`: string (date-time) or null, required. Due date, when set.
  - `completed`: boolean, required. Public name for the `finished` column.
  - `priority`: one of `"none"`, `"low"`, `"medium"`, `"high"`, required. How urgent the task is: `none`, `low`, `medium` or `high`, from least to most urgent. `none` when unset.
  - `parentId`: string or null, required. The parent task's id for a subtask, `null` for a top-level task.
  - `createdAt`: string (date-time), required. Creation timestamp.
  - `updatedAt`: string (date-time), required. Last update timestamp.

#### Example

```json
{
  "data": {
    "id": "4d3c2b1a-0f9e-4d8c-9b7a-6f5e4d3c2b1a",
    "name": "Finish lab report",
    "description": "Include the error analysis.",
    "type": "Generic",
    "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
    "dueDate": "2026-02-10T17:00:00.000Z",
    "completed": false,
    "priority": "high",
    "parentId": null,
    "createdAt": "2026-01-15T09:30:00.000Z",
    "updatedAt": "2026-02-01T12:00:00.000Z"
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No task with that id is in the trash for this key's user.
- `405`: Method not allowed. Use a method from the Allow header.
- `409`: The Idempotency-Key was reused with a different request body, or a request with the same key is still in progress.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Add a subtask

`POST https://workroad.app/api/v1/tasks/{taskId}/subtasks`

Adds a checklist item to a top-level task. The subtask takes the parent's type and subject. A task can have at most 50 subtasks, and subtasks cannot have their own.

- MCP tool: `createSubtask` on `https://workroad.app/api/mcp`
- Required scopes: tasks:create
- Test mode: `POST https://workroad.app/api/test/v1/tasks/{taskId}/subtasks` checks the request and returns the example response without changing anything.

### Path parameters

- `taskId`: string (uuid), required. Task id, scoped to the key's user.

### Headers

- `Idempotency-Key`: string, optional, 1-255 visible ASCII characters. Makes the POST safe to retry: a retry with the same key and body replays the first response, and the same key with a different body returns 409 `idempotency_conflict`. Kept for 24 hours. Over MCP it is the `idempotencyKey` argument.

### Request body

JSON.

- `name`: string, required, 1-64 characters

### Response

`201` Created

- `data`: object, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `description`: string or null, required
  - `type`: string, required. Free-form task type from the user's task settings.
  - `subjectId`: string or null, required. Subject id, when set.
  - `dueDate`: string (date-time) or null, required. Due date, when set.
  - `completed`: boolean, required. Public name for the `finished` column.
  - `priority`: one of `"none"`, `"low"`, `"medium"`, `"high"`, required. How urgent the task is: `none`, `low`, `medium` or `high`, from least to most urgent. `none` when unset.
  - `parentId`: string or null, required. The parent task's id for a subtask, `null` for a top-level task.
  - `createdAt`: string (date-time), required. Creation timestamp.
  - `updatedAt`: string (date-time), required. Last update timestamp.

#### Example

```json
{
  "data": {
    "id": "8b7a6f5e-4d3c-4b2a-9f1e-0d9c8b7a6f5e",
    "name": "Plot the titration curve",
    "description": null,
    "type": "Generic",
    "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
    "dueDate": "2026-02-10T17:00:00.000Z",
    "completed": false,
    "priority": "none",
    "parentId": "4d3c2b1a-0f9e-4d8c-9b7a-6f5e4d3c2b1a",
    "createdAt": "2026-01-15T09:30:00.000Z",
    "updatedAt": "2026-02-01T12:00:00.000Z"
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid body, or the parent already has 50 subtasks.
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No top-level task with that id exists for this key's user.
- `405`: Method not allowed. Use a method from the Allow header.
- `409`: The Idempotency-Key was reused with a different request body, or a request with the same key is still in progress.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## List task types

`GET https://workroad.app/api/v1/task-types`

The task types from the user's task settings, in their configured order. Use one as a task's `type`.

- MCP tool: `listTaskTypes` on `https://workroad.app/api/mcp`
- Required scopes: tasks:view
- Test mode: `GET https://workroad.app/api/test/v1/task-types` checks the request and returns the example response without changing anything.

### Response

`200` OK

- `data`: array of string, required

#### Example

```json
{
  "data": [
    "Generic",
    "Homework",
    "Test"
  ]
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error
