> ## Documentation Index
> Fetch the complete documentation index at: https://docs.crazygoldfish.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Create action plans with the API

> Turn the results of an evaluated exam into a teaching plan for the class and a personalized plan for each student using the Action Plans API.

Action Plans read the results of an exam you already evaluated and say what to do next. One plan is for the teacher and covers the whole class. The other is for a single student. Each plan takes two API calls: one to start it, one to collect it.

This guide is for developers. For a non-technical overview, see [Teacher Action Plan features](/products/teacher-action-plan/features) and [Student Action Plan features](/products/student-action-plan/features).

| Step | You call                    | You get back                                            | Who reads it |
| ---- | --------------------------- | ------------------------------------------------------- | ------------ |
| 1    | Start a teacher action plan | The exam ID, while the job runs                         |              |
| 2    | Get the teacher action plan | One action per question the class lost marks on         | The teacher  |
| 3    | Start a student action plan | The answer sheet ID, while the job runs                 |              |
| 4    | Get the student action plan | Next steps, a skill to focus on, and an encouraging tip | The student  |

Steps 1 and 2 run once per exam. Steps 3 and 4 run once per student.

<Info>
  Each call starts a background job. Wait until the job has finished before you show the plan. The `wait_for` helper below does this for you.
</Info>

## Set up

You need an access token. If you don't have one, follow the [Quickstart](/api-reference/quickstart).

You also need an exam whose answer sheets are evaluated. Action plans are built from the answers where a student scored less than the full marks, and from the feedback the evaluation left on those answers. See [Get scores for review](/api-reference/endpoint/evaluation-platform/v1-pre-provisional-scores).

<CodeGroup>
  ```python Python theme={null}
  import time

  import requests

  BASE_URL = "https://api.crazygoldfish.com"
  HEADERS = {"Authorization": "Bearer YOUR_ACCESS_TOKEN"}

  EXAM_ID = "d19e6868-98dd-4274-a2a5-4a616dd93ea7"
  DONE = ("Completed", "Partial")


  def wait_for(path):
      """Check the plan every 5 seconds until the job finishes."""
      for _ in range(120):  # Give up after 10 minutes
          response = requests.get(BASE_URL + path, headers=HEADERS)
          response.raise_for_status()
          plan = response.json()["data"]
          if plan["status"] in DONE:
              return plan
          if plan["status"] == "Failed":
              raise RuntimeError(f"{path} failed")
          time.sleep(5)
      raise TimeoutError(f"{path} took longer than 10 minutes")
  ```

  ```javascript JavaScript theme={null}
  const BASE_URL = "https://api.crazygoldfish.com";
  const HEADERS = { Authorization: "Bearer YOUR_ACCESS_TOKEN" };

  const EXAM_ID = "d19e6868-98dd-4274-a2a5-4a616dd93ea7";
  const DONE = ["Completed", "Partial"];

  // Check the plan every 5 seconds until the job finishes
  async function waitFor(path) {
    for (let i = 0; i < 120; i++) { // Give up after 10 minutes
      const response = await fetch(BASE_URL + path, { headers: HEADERS });
      if (!response.ok) throw new Error(await response.text());
      const { data } = await response.json();
      if (DONE.includes(data.status)) return data;
      if (data.status === "Failed") throw new Error(`${path} failed`);
      await new Promise((resolve) => setTimeout(resolve, 5000));
    }
    throw new Error(`${path} took longer than 10 minutes`);
  }
  ```
</CodeGroup>

<Warning>
  Both GET endpoints return `200` for every status, including `Failed`. Always branch on `data.status`.
</Warning>

A plan reports one of four statuses:

| `data.status` | Meaning                                                                                                               | What to do                                                                             |
| ------------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `In Progress` | The job is still running, and `action_plan` is an empty object                                                        | Poll again                                                                             |
| `Completed`   | The whole plan is ready                                                                                               | Show it                                                                                |
| `Partial`     | A teacher plan where some questions were analyzed and others failed, so the plan covers fewer questions than the exam | Show what came back, or start the plan again. This is a finished job, so stop polling. |
| `Failed`      | Nothing could be written                                                                                              | Start the plan again                                                                   |

That is why the helper above treats `Partial` as done. A student plan is a single request, so only a teacher plan reports `Partial`.

## Step 1: Start the teacher action plan

Send the exam's UUID. The job reads every answer sheet in the exam, groups the answers that lost marks by question number, and writes one action per question.

<CodeGroup>
  ```python Python theme={null}
  response = requests.post(
      BASE_URL + "/action-plan/v1/k12/teacher",
      headers=HEADERS,
      json={"exam_id": EXAM_ID},
  )
  response.raise_for_status()
  ```

  ```javascript JavaScript theme={null}
  const started = await fetch(`${BASE_URL}/action-plan/v1/k12/teacher`, {
    method: "POST",
    headers: { ...HEADERS, "Content-Type": "application/json" },
    body: JSON.stringify({ exam_id: EXAM_ID }),
  });
  if (!started.ok) throw new Error(await started.text());
  ```
</CodeGroup>

<Accordion title="What you get back">
  ```json theme={null}
  {
    "detail": [
      {
        "id": "d19e6868-98dd-4274-a2a5-4a616dd93ea7",
        "msg": "Teacher action plan initiated successfully."
      }
    ]
  }
  ```

  `id` is the exam ID you sent, not a new job ID. Keep using the exam ID.
</Accordion>

## Step 2: Get the teacher action plan

Poll the same exam ID until the job finishes.

<CodeGroup>
  ```python Python theme={null}
  plan = wait_for(f"/action-plan/v1/k12/teacher/{EXAM_ID}")

  for action in plan["action_plan"]:
      print(action["question_number"], action["action_type"])
      print(action["action"])
  ```

  ```javascript JavaScript theme={null}
  const plan = await waitFor(`/action-plan/v1/k12/teacher/${EXAM_ID}`);

  for (const action of plan.action_plan) {
    console.log(action.question_number, action.action_type);
    console.log(action.action);
  }
  ```
</CodeGroup>

<Accordion title="What you get back">
  ```json theme={null}
  {
    "data": {
      "id": "d19e6868-98dd-4274-a2a5-4a616dd93ea7",
      "exam_name": "Grade 10 Science Half Yearly",
      "status": "Completed",
      "created_at": "2026-09-17T11:24:38.512431",
      "action_plan": [
        {
          "question_number": "3",
          "students": ["S-1042", "S-1077", "S-1103"],
          "overall_insight": "Most students could not apply the thumb rule.",
          "action_type": "Remediation",
          "action": "Re-teach the rule on a straight wire, then on a loop."
        }
      ]
    }
  }
  ```

  While the job runs, `action_plan` is an empty object `{}`, not an empty array. `action_type` is `Reinforcement`, `Remediation`, or `Extension`.
</Accordion>

## Step 3: Start a student action plan

One call covers one student, so loop over the exam's answer sheets. Get their IDs from [List answer sheets](/api-reference/endpoint/evaluation-platform/v1-get-student-answer-sheet).

<Warning>
  The request field is `ans_sheet_id`. The GET path calls the same value `answer_sheet_id`.
</Warning>

<CodeGroup>
  ```python Python theme={null}
  ANSWER_SHEET_IDS = ["8a2f4c1e-5b3d-4e6f-9a7b-0c1d2e3f4a5b"]

  for sheet_id in ANSWER_SHEET_IDS:
      response = requests.post(
          BASE_URL + "/action-plan/v1/k12/student",
          headers=HEADERS,
          json={"ans_sheet_id": sheet_id},
      )
      response.raise_for_status()
  ```

  ```javascript JavaScript theme={null}
  const ANSWER_SHEET_IDS = ["8a2f4c1e-5b3d-4e6f-9a7b-0c1d2e3f4a5b"];

  for (const sheetId of ANSWER_SHEET_IDS) {
    const res = await fetch(`${BASE_URL}/action-plan/v1/k12/student`, {
      method: "POST",
      headers: { ...HEADERS, "Content-Type": "application/json" },
      body: JSON.stringify({ ans_sheet_id: sheetId }),
    });
    if (!res.ok) throw new Error(await res.text());
  }
  ```
</CodeGroup>

## Step 4: Get the student action plans

Poll each answer sheet ID. The plan comes back as an array with one item in it.

<CodeGroup>
  ```python Python theme={null}
  for sheet_id in ANSWER_SHEET_IDS:
      result = wait_for(f"/action-plan/v1/k12/student/{sheet_id}")
      student_plan = result["action_plan"][0]
      print(result["student_name"], student_plan["skill_focus"])
      print(student_plan["action"])
  ```

  ```javascript JavaScript theme={null}
  for (const sheetId of ANSWER_SHEET_IDS) {
    const result = await waitFor(`/action-plan/v1/k12/student/${sheetId}`);
    const studentPlan = result.action_plan[0];
    console.log(result.student_name, studentPlan.skill_focus);
    console.log(studentPlan.action);
  }
  ```
</CodeGroup>

<Accordion title="What you get back">
  ```json theme={null}
  {
    "data": {
      "id": "4b7f2a91-6c3d-4e58-9a2b-7d1e0c5f8a34",
      "status": "Completed",
      "student_id": "S-1042",
      "student_name": "Aarav Sharma",
      "created_at": "2026-09-17T11:31:02.884190",
      "action_plan": [
        {
          "overall_insight": "Aarav explains the idea but struggles to apply it.",
          "overall_action_type": "Remediation",
          "action": "Draw the diagram before writing each answer.",
          "skill_focus": "Application",
          "confidence_booster_tip": "You explain the idea well already.",
          "questions": [
            {
              "question_number": "3",
              "action_type": "Remediation",
              "action": "Practice the thumb rule on a wire, then on a loop."
            }
          ]
        }
      ]
    }
  }
  ```

  `data.id` is the plan's own ID. No endpoint takes it, so keep using the answer sheet ID.
</Accordion>

## Good to know

<AccordionGroup>
  <Accordion title="Limits" icon="gauge">
    | Limit             | Value                                                                                                                                                                                  |
    | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | Source data       | Only answers that scored less than the full marks, with the evaluation's feedback                                                                                                      |
    | Teacher plan      | One call covers the whole exam. The exam needs at least one answer sheet.                                                                                                              |
    | Student plan      | One call covers one answer sheet                                                                                                                                                       |
    | Starting plans    | Daily limit per account. One counter covers the whole account and both POST endpoints share it, so teacher and student plans spend the same allowance. It resets at 00:01 server time. |
    | Rejected requests | Still count. The counter is spent when the request arrives, before the exam or answer sheet is checked, so a request rejected with `400` uses up one of your calls.                    |
    | Repeat calls      | Allowed. Each call creates a new plan, and the GET call returns the newest one.                                                                                                        |
    | Visibility        | You only see plans your own API user started                                                                                                                                           |
  </Accordion>

  <Accordion title="Use webhooks instead of polling" icon="bell">
    Register a webhook for each event, then fetch the plan when the event arrives. See [Async jobs and webhooks](/api-reference/async-jobs-and-webhooks#register-a-webhook).

    | Plan    | Event                 | `id` in the payload |
    | ------- | --------------------- | ------------------- |
    | Teacher | `teacher_action_plan` | The exam ID         |
    | Student | `student_action_plan` | The answer sheet ID |

    Both events are sent for `Completed`, `Partial`, and `Failed`, and only to the API user who started the plan.
  </Accordion>

  <Accordion title="Troubleshooting" icon="wrench">
    | Error                                                | Fix                                                                                                  |
    | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
    | `422` `Value error, Exam with ID ... does not exist` | Use the exam `unique_id` from the Exam Evaluation API                                                |
    | `422` `Field required` for `ans_sheet_id`            | The body field is `ans_sheet_id`, not `answer_sheet_id`                                              |
    | The teacher plan request fails                       | The exam needs evaluated answer sheets. Add and evaluate them first.                                 |
    | The student plan request fails                       | Check the ID is the answer sheet ID you created the plan with.                                       |
    | `404` `Action plan not found ...`                    | Start the plan first, with the same API user                                                         |
    | `429` `Daily rate limit exceeded`                    | Try again after 00:01 server time                                                                    |
    | `status` is `Failed` right away                      | No student lost marks in the exam, so there was nothing to plan                                      |
    | Still `In Progress` after 10 minutes                 | Start the plan again and [contact us](https://www.crazygoldfish.com/#comp-lxumousa) with the exam ID |
  </Accordion>
</AccordionGroup>

## Related

<CardGroup cols={2}>
  <Card title="Action Plans API reference" icon="code" href="/api-reference/endpoint/action-plans/k12-v1-teacher-action-plan-post">
    Every field, response, and error.
  </Card>

  <Card title="Evaluate an exam" icon="file-pen" href="/api-reference/endpoint/evaluation-platform/v1-exam">
    Create the exam and evaluate the answer sheets first.
  </Card>

  <Card title="Teacher Action Plan" icon="chalkboard-user" href="/products/teacher-action-plan/features">
    What teachers see, without the code.
  </Card>

  <Card title="Student Action Plan" icon="user-graduate" href="/products/student-action-plan/features">
    What students see, without the code.
  </Card>
</CardGroup>
