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

# Grade an open-ended answer

> Wire AI grading with a rubric, then put a human in the loop.

This guide covers the most common high-stakes flow: a student submits
an essay, the AI grades it, a teacher reviews, the gradebook updates.

## 1. Configure the assignment

<CodeGroup>
  ```ts TypeScript theme={null}
  const assignment = await hk.assignments.create({
    lesson_id: "lsn_123",
    type: "essay",
    title: "Explain mass-energy equivalence",
    rubric: {
      criteria: [
        { name: "clarity",  weight: 0.4 },
        { name: "accuracy", weight: 0.4 },
        { name: "sources",  weight: 0.2 },
      ],
      scale: { min: 0, max: 10 },
    },
  });

  await hk.assignments.configure(assignment.id, {
    ai_evaluator: { enabled: true, model: "claude-sonnet-4-6" },
    human_review: "required",   // student doesn't see grade until reviewed
    feedback: { tone: "encouraging", verbosity: "normal" },
  });
  ```

  ```php PHP theme={null}
  $assignment = $hk->assignments->create([
      'lesson_id' => 'lsn_123',
      'type' => 'essay',
      'title' => 'Explain mass-energy equivalence',
      'rubric' => [
          'criteria' => [
              ['name' => 'clarity',  'weight' => 0.4],
              ['name' => 'accuracy', 'weight' => 0.4],
              ['name' => 'sources',  'weight' => 0.2],
          ],
          'scale' => ['min' => 0, 'max' => 10],
      ],
  ]);

  $hk->assignments->configure($assignment->id, [
      'ai_evaluator' => ['enabled' => true, 'model' => 'claude-sonnet-4-6'],
      'human_review' => 'required',    // student doesn't see grade until reviewed
      'feedback' => ['tone' => 'encouraging', 'verbosity' => 'normal'],
  ]);
  ```
</CodeGroup>

## 2. Receive a submission

The student app sends the response:

<CodeGroup>
  ```ts TypeScript theme={null}
  const submission = await hk.submissions.create({
    assignment_id: assignment.id,
    student_id: "usr_student_42",
    content: { text: studentEssay },
  });
  ```

  ```php PHP theme={null}
  $submission = $hk->submissions->create([
      'assignment_id' => $assignment->id,
      'student_id' => 'usr_student_42',
      'content' => ['text' => $studentEssay],
  ]);
  ```
</CodeGroup>

If the assignment accepts files:

<CodeGroup>
  ```ts TypeScript theme={null}
  await hk.submissions.create({
    assignment_id: assignment.id,
    student_id: "usr_student_42",
    content: { text: "" },
    files: [{ name: "essay.pdf", body: fileBlob, content_type: "application/pdf" }],
  });
  ```

  ```php PHP theme={null}
  $hk->submissions->create([
      'assignment_id' => $assignment->id,
      'student_id' => 'usr_student_42',
      'content' => ['text' => ''],
      'files' => [[
          'name' => 'essay.pdf',
          'body' => fopen('/path/to/essay.pdf', 'r'),
          'content_type' => 'application/pdf',
      ]],
  ]);
  ```
</CodeGroup>

## 3. Trigger AI grading

<CodeGroup>
  ```ts TypeScript theme={null}
  await hk.submissions.gradeWithAi(submission.id);
  ```

  ```php PHP theme={null}
  $hk->submissions->gradeWithAi($submission->id);
  ```
</CodeGroup>

This is async. Subscribe to the `submission.graded` webhook for
production, or poll for prototypes:

<CodeGroup>
  ```ts TypeScript theme={null}
  const graded = await hk.poll(
    () => hk.submissions.retrieve(submission.id),
    { until: s => s.ai_status === "ready" || s.ai_status === "error" },
  );
  ```

  ```php PHP theme={null}
  $graded = $hk->poll(
      fn () => $hk->submissions->retrieve($submission->id),
      ['until' => fn ($s) => $s->ai_status === 'ready' || $s->ai_status === 'error'],
  );
  ```
</CodeGroup>

## 4. Show the teacher

<CodeGroup>
  ```ts TypeScript theme={null}
  console.log(graded.grade_ai);             // 8.5
  console.log(graded.grading_rationale);    // markdown breakdown
  console.log(graded.grading_breakdown);    // [{criterion, score, max}, ...]
  ```

  ```php PHP theme={null}
  echo $graded->grade_ai;                   // 8.5
  echo $graded->grading_rationale;          // markdown breakdown
  print_r($graded->grading_breakdown);      // [{criterion, score, max}, ...]
  ```
</CodeGroup>

Render `grading_rationale` as markdown next to the student's text. The
teacher gets context, not a black box.

## 5. Teacher confirms or overrides

<CodeGroup>
  ```ts TypeScript theme={null}
  await hk.submissions.update(submission.id, {
    grade_manual: 9.0,                                     // override
    grader_comments: "Great link to E=mc² in the closing.", // markdown
    human_review_status: "reviewed",
  });
  ```

  ```php PHP theme={null}
  $hk->submissions->update($submission->id, [
      'grade_manual' => 9.0,                                       // override
      'grader_comments' => 'Great link to E=mc² in the closing.',  // markdown
      'human_review_status' => 'reviewed',
  ]);
  ```
</CodeGroup>

If the teacher accepts the AI's grade verbatim:

<CodeGroup>
  ```ts TypeScript theme={null}
  await hk.submissions.update(submission.id, {
    grade_manual: graded.grade_ai,
    human_review_status: "reviewed",
  });
  ```

  ```php PHP theme={null}
  $hk->submissions->update($submission->id, [
      'grade_manual' => $graded->grade_ai,
      'human_review_status' => 'reviewed',
  ]);
  ```
</CodeGroup>

Or, if you want to skip the override and trust the AI:

<CodeGroup>
  ```ts TypeScript theme={null}
  await hk.submissions.update(submission.id, { human_review_status: "reviewed" });
  // `grade_manual` stays null; the gradebook reads from `grade_ai`.
  ```

  ```php PHP theme={null}
  $hk->submissions->update($submission->id, ['human_review_status' => 'reviewed']);
  // `grade_manual` stays null; the gradebook reads from `grade_ai`.
  ```
</CodeGroup>

## 6. Read the final grade

The SDK abstracts the precedence rule:

<CodeGroup>
  ```ts TypeScript theme={null}
  const sub = await hk.submissions.retrieve(submission.id);
  console.log(sub.final_grade);   // grade_manual ?? grade_ai
  ```

  ```php PHP theme={null}
  $sub = $hk->submissions->retrieve($submission->id);
  echo $sub->final_grade;   // grade_manual ?? grade_ai
  ```
</CodeGroup>

`final_grade` is null while `human_review_status === "pending"` if the
assignment requires human review. That's what your student-facing UI
should read.

## At scale: grade a whole class

<CodeGroup>
  ```ts TypeScript theme={null}
  const subs = (await hk.submissions.list({ assignment_id: assignment.id })).data;

  await Promise.all(
    subs.map(s => hk.submissions.gradeWithAi(s.id)),
  );

  await hk.poll(
    () => hk.submissions.list({ assignment_id: assignment.id }),
    { until: page => page.data.every(s => s.ai_status === "ready") },
  );

  // Now show the teacher a queue of submissions ranked by edge cases:
  const needsReview = subs.filter(s => s.grading_breakdown.some(c => c.score / c.max < 0.5));
  ```

  ```php PHP theme={null}
  $subs = $hk->submissions->list(['assignment_id' => $assignment->id])->data;

  foreach ($subs as $s) {
      $hk->submissions->gradeWithAi($s->id);
  }

  $hk->poll(
      fn () => $hk->submissions->list(['assignment_id' => $assignment->id]),
      [
          'until' => fn ($page) => array_reduce(
              $page->data,
              fn ($acc, $s) => $acc && $s->ai_status === 'ready',
              true,
          ),
      ],
  );

  // Now show the teacher a queue of submissions ranked by edge cases:
  $needsReview = array_filter($subs, function ($s) {
      foreach ($s->grading_breakdown as $c) {
          if ($c->score / $c->max < 0.5) return true;
      }
      return false;
  });
  ```
</CodeGroup>

## Tips

<Tip>
  **Pin the model.** AI grades drift between model versions. If you need
  audit-grade reproducibility, pass `model: "claude-sonnet-4-6"` (or
  similar) in the assignment config.
</Tip>

<Tip>
  **Show the rubric to the student** before they submit. A 30% increase
  in rubric-aligned answers is typical.
</Tip>

<Warning>
  **Never auto-publish AI grades on consequential assessments.** The
  `human_review: "required"` flag exists for a reason. Use it.
</Warning>
