# Create a timetable with its lessons

Source: https://workroad.app/docs/api/create-timetable

`POST https://workroad.app/api/v1/schedules/timetable`

Creates a timetable kept in WorkRoad with its periods and every day's lessons in one call, as the app's setup does: the first timetable becomes the default, and the days with lessons are the days it shows. Use it to save a timetable you read from a photo, a PDF or what the user typed; it uses no AI. Link lessons to the user's subjects with `subjectId` or `subjectName`, and list the subjects first (`GET /subjects`) so the names match. The timetable is saved whole or not at all.

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

- MCP tool: `createTimetable` on `https://workroad.app/api/mcp`
- Required scopes: schedule:create
- Test mode: `POST https://workroad.app/api/test/v1/schedules/timetable` checks the request and returns the example response without changing anything.
- Next: read it back for the week with `getScheduleWeek`.

## 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-64 characters. 2-64 characters. Unique among the user's active schedules.
- `scheduleType`: one of `"defined"`, `"dynamic"`, required. `defined`: every day shares one grid of `periods`, and each day lists one entry per period (`null` for a free period). `dynamic`: no periods; each day lists its lessons, each with `timeStart` and `timeEnd`.
- `firstDay`: integer, 0 to 6. The weekday the timetable starts on, which is the first entry of `days`. Defaults to 1 (Monday).
- `periods`: array of objects, 1-30 items. `defined` only: 1-30 periods, in order, not overlapping.
  - `periodName`: string, required, up to 200 characters. Such as `Period 1` or `Break`.
  - `time`: object or null, required. `HH:MM` start and end, or `null` when the timetable shows no time.
    - `start`: string, required, up to 5 characters
    - `end`: string, required, up to 5 characters
- `days`: array of array of object or null, required, 1-7 items. One entry per weekday from `firstDay`, up to 7: the day's lessons in order. For a `defined` timetable every day has exactly one entry per period. A day with no lessons is an empty list (`dynamic`) or all `null` (`defined`), and days left off the end are free.
  - `lessonName`: string, up to 200 characters, default `""`. What the timetable calls the lesson, such as `Maths` or `Lunch`. Empty for a free period.
  - `subjectId`: string (uuid). The user's subject the lesson is for.
  - `subjectName`: string, 1-200 characters. The course by name. It links the lesson to the user's active subject with that name, ignoring case and accents. A name that matches none is created as a subject when `createSubjectsIfMissing` is true, and otherwise kept as written on the timetable without a link.
  - `room`: string, up to 200 characters, default `""`. The room, or empty.
  - `teacher`: string, up to 200 characters, default `""`. The teacher, or empty.
  - `timeStart`: string, up to 5 characters. A `dynamic` lesson's start. `HH:MM`, 24-hour.
  - `timeEnd`: string, up to 5 characters. A `dynamic` lesson's end. `HH:MM`, 24-hour.
- `createSubjectsIfMissing`: boolean. Creates a subject for each `subjectName` the user doesn't have yet, so the lessons link to it. Needs the `subjects:create` scope.

## Response

`201` Created

- `data`: object, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `type`: one of `"DEFINED"`, `"DYNAMIC"`, required. `DEFINED` timetables fill numbered periods from `structure`. `DYNAMIC` ones hold lessons with their own start and end times.
  - `external`: string or null, required. The integration an external schedule is read from, such as `aru-evision` or `google-calendar`. Null for a schedule kept in WorkRoad.
  - `owned`: boolean, required. False when another user shared the schedule with this user.
  - `editable`: boolean, required. Whether the lesson grid (`structure` and elements) can be written: true only for the owner's `DEFINED` schedules kept in WorkRoad. Shared, external and `DYNAMIC` schedules are false.
  - `firstDay`: integer, required. The weekday (0 = Sunday) the timetable starts on. `days` start there. Always 1 (Monday) for external schedules.
  - `selectedDays`: array of integer, required. Weekdays the schedule shows, 0 (Sunday) to 6 (Saturday).
  - `issue`: one of `"failed"` or null, required. `failed` when an external schedule's integration could not be read. `days` is then empty. Null otherwise.
  - `structure`: object or null, required. The periods of a `DEFINED` schedule. Null for `DYNAMIC` ones and failed reads.
    - `periods`: array of objects, required
      - `index`: integer, required. The period number elements refer to.
      - `name`: string, required
      - `start`: string or null, required. Start time (HH:MM), or null.
      - `end`: string or null, required. End time (HH:MM), or null.
  - `days`: array of objects, required. An external schedule's days are the current week's.
    - `day`: integer, required. Weekday, 0 (Sunday) to 6 (Saturday).
    - `elements`: array of objects, required
      - `id`: string, required. Element id. A uuid on schedules kept in WorkRoad; the provider's id on external ones.
      - `period`: integer, required. The period index on a `DEFINED` schedule, or the lesson's order in the day.
      - `lessonName`: string or null, required. Null for an empty period.
      - `subjectId`: string or null, required. The subject the lesson is filed under. On an external schedule, the subject this user linked the lesson to. Null when it has none or when `subjectLabel` is set.
      - `subjectLabel`: string or null, required. Free text from older timetables, kept as typed. Null when the lesson has a `subjectId` or no subject. It cannot be written through the API; sending `subjectId` replaces it.
      - `room`: string or null, required
      - `teacher`: string or null, required
      - `timeStart`: string or null, required. Start time (HH:MM), when set.
      - `timeEnd`: string or null, required. End time (HH:MM), when set.

### Example

```json
{
  "data": {
    "id": "4d3c2b1a-0f9e-4d8c-8b7a-6f5e4d3c2b1a",
    "name": "Year 12",
    "type": "DEFINED",
    "external": null,
    "owned": true,
    "editable": true,
    "firstDay": 1,
    "selectedDays": [
      1,
      2,
      3,
      4,
      5
    ],
    "issue": null,
    "structure": {
      "periods": [
        {
          "index": 0,
          "name": "Period 1",
          "start": "09:00",
          "end": "10:00"
        },
        {
          "index": 1,
          "name": "Period 2",
          "start": "10:00",
          "end": "11:00"
        }
      ]
    },
    "days": [
      {
        "day": 1,
        "elements": [
          {
            "id": "6e5d4c3b-2a19-4f08-9e7d-6c5b4a392817",
            "period": 0,
            "lessonName": "Biology",
            "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
            "subjectLabel": null,
            "room": "S2",
            "teacher": "Dr Patel",
            "timeStart": null,
            "timeEnd": null
          },
          {
            "id": "7f6e5d4c-3b2a-4190-8f8e-7d6c5b4a3928",
            "period": 1,
            "lessonName": null,
            "subjectId": null,
            "subjectLabel": null,
            "room": null,
            "teacher": null,
            "timeStart": null,
            "timeEnd": null
          }
        ]
      }
    ]
  }
}
```

## Errors

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

- `400`: `invalid_request` with `details.reason`: `invalid-timetable`, `time-invalid`, `grid-mismatch` or `unknown-subject`.
- `401`: Missing, invalid, disabled or expired API key
- `403`: `createSubjectsIfMissing` 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`: The user already has an active schedule with this name (`details.reason` is `name-taken`). 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
