# Create a practice test from your questions

Source: https://workroad.app/docs/api/create-test-from-questions

`POST https://workroad.app/api/v1/tests/from-questions`

Saves a practice test whose questions you wrote, in the shape WorkRoad's own generator writes them, and answers 201 with the test, ready to take: start it with `POST /tests/{testId}/attempts`. Nothing is generated, so it uses no AI. Multiple choice, true/false and fill-in questions are graded without AI too; short answers go to AI grading when an attempt is submitted, which counts as an AI use on each submit, so prefer the other kinds unless the user wants written answers. Read the material first (`GET /notes/{noteId}`, `GET /sets/{setId}`) and write questions it answers.

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

- MCP tool: `createTestFromQuestions` on `https://workroad.app/api/mcp`
- Required scopes: tests:create
- Test mode: `POST https://workroad.app/api/test/v1/tests/from-questions` checks the request and returns the example response without changing anything.
- Next: start it with `startTestAttempt`, or give the user the test's name to take it in the app.

## 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-120 characters. The test's name, 1-120 characters.
- `questions`: array of objects, required, 1-50 items. 1-50 questions in the order they are asked, written in the shape WorkRoad's own test generator writes them.
  - `kind`: one of `"multiple-choice"`, `"short-answer"`, `"true-false"`, `"fill-blank"`, required. `multiple-choice`, `true-false`, `fill-blank` or `short-answer`. Short answers are graded by WorkRoad's AI when the attempt is submitted, which counts as an AI use; the other kinds are graded without AI.
  - `prompt`: string, required, 1-2000 characters. The question as the student reads it.
  - `choices`: array of string, 2-8 items. Multiple choice only: 8 or fewer distinct options, one of them exactly `answer`. WorkRoad shuffles them once when it saves the test.
  - `answer`: string, required, 1-2000 characters. The correct answer: the right choice's exact text, `true` or `false`, the word or phrase that fills the blank, or a model answer for a short answer.
  - `explanation`: string, required, 1-4000 characters. Why the answer is right, shown after the attempt is submitted.
  - `difficulty`: one of `"recall"`, `"apply"`, `"analyse"`, required. `recall`, `apply` or `analyse`.
- `timerSecs`: integer or null, 60 to 7200, default `null`. A time limit in seconds, 60-7200, counted from the start of each attempt. Null or omitted for no limit.
- `sourceSetIds`: array of string, up to 10 items, default `[]`. The user's study sets the questions cover, up to 10. Optional; they label the test.
- `sourceNoteIds`: array of string, up to 10 items, default `[]`. The user's notes the questions cover, up to 10. Optional.

## Response

`201` Created

- `data`: object, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `assisted`: boolean, required
  - `status`: one of `"draft"`, `"generating"`, `"ready"`, `"failed"`, required. `generating` until the questions are written, then `ready`. `failed` when generation failed or could not start; `error` says why.
  - `error`: string or null, required. Why generation failed.
  - `createdAt`: string (date-time), required. When the test was created.
  - `generationJobId`: string or null, required. The latest generation job. Poll `GET /jobs/{jobId}` while `status` is `generating`.
  - `questionsVersion`: integer, required. Goes up each time the questions are generated again.
  - `questionCount`: integer, required
  - `kindCounts`: array of objects, required. How many questions of each kind the test has.
    - `kind`: one of `"multiple-choice"`, `"short-answer"`, `"true-false"`, `"fill-blank"`, required. The question's kind. `short-answer` questions are graded by AI.
    - `count`: integer, required
  - `generating`: object or null, required. While `status` is `generating`, the questions written so far, without answers. Null otherwise.
    - `total`: integer, required. Questions asked for.
    - `questions`: array of objects, required
      - `kind`: one of `"multiple-choice"`, `"short-answer"`, `"true-false"`, `"fill-blank"`, required. The question's kind. `short-answer` questions are graded by AI.
      - `prompt`: string, required
      - `difficulty`: string, required
      - `choiceCount`: integer, required
  - `requestedLength`: integer, required
  - `kinds`: array of one of `"MCQ"`, `"TRUE_FALSE"`, `"SHORT"`, `"FILL_BLANK"`, required
  - `timerSecs`: integer or null, required. The time limit, or null.
  - `modelTier`: string, required
  - `sourceSetIds`: array of string, required
  - `sourceNoteIds`: array of string, required
  - `attempts`: array of objects, required. The 25 most recent attempts, newest first.
    - `id`: string (uuid), required
    - `assisted`: boolean, required
    - `score`: object or null, required. Correct answers out of all questions. Null until grading ends.
      - `num`: integer, required
      - `den`: integer, required
    - `gradingStatus`: one of `"not_required"`, `"pending"`, `"graded"`, `"failed"`, required. `not_required` when every answer was graded on submit, `pending` while AI grades the short answers, `graded` once it has, and `failed` when AI grading could not run. The app then lets the user mark those answers themselves.
    - `questionsVersion`: integer, required. The version of the questions the attempt answered.
    - `startedAt`: string (date-time), required. When the attempt started.
    - `finishedAt`: string (date-time) or null, required. When it was submitted. Null while it is open.
    - `gradedAt`: string (date-time) or null, required. When grading ended.

### Example

```json
{
  "data": {
    "id": "6e5d4c3b-2a19-4f08-8e7d-6c5b4a392817",
    "name": "Cell biology quiz",
    "assisted": false,
    "status": "ready",
    "error": null,
    "createdAt": "2026-01-15T09:30:00.000Z",
    "generationJobId": "7d6c5b4a-3f2e-4d1c-8b0a-9f8e7d6c5b4a",
    "questionsVersion": 1,
    "questionCount": 2,
    "kindCounts": [
      {
        "kind": "multiple-choice",
        "count": 1
      },
      {
        "kind": "short-answer",
        "count": 1
      }
    ],
    "generating": null,
    "requestedLength": 2,
    "kinds": [
      "MCQ",
      "SHORT"
    ],
    "timerSecs": 600,
    "modelTier": "scholar",
    "sourceSetIds": [],
    "sourceNoteIds": [
      "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e"
    ],
    "attempts": [
      {
        "id": "1f2e3d4c-5b6a-4798-8a7b-6c5d4e3f2a1b",
        "assisted": false,
        "score": {
          "num": 1,
          "den": 2
        },
        "gradingStatus": "graded",
        "questionsVersion": 1,
        "startedAt": "2026-01-15T09:30:00.000Z",
        "finishedAt": "2026-02-01T12:00:00.000Z",
        "gradedAt": "2026-02-01T12:00:00.000Z"
      }
    ]
  }
}
```

## Errors

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

- `400`: `invalid_request`: the body fails validation, or `details.field` is `questions`, `details.index` the question and `details.reason` why it can't be graded as written: `choices-missing`, `choices-unexpected`, `choice-empty`, `choices-duplicate`, `answer-not-a-choice` or `true-false-answer`. Also `sourceSetIds` `unknown-set` and `sourceNoteIds` `unknown-note`.
- `401`: Missing, invalid, disabled or expired API key
- `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
