Skip to main content
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

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" },
});
$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'],
]);

2. Receive a submission

The student app sends the response:
const submission = await hk.submissions.create({
  assignment_id: assignment.id,
  student_id: "usr_student_42",
  content: { text: studentEssay },
});
$submission = $hk->submissions->create([
    'assignment_id' => $assignment->id,
    'student_id' => 'usr_student_42',
    'content' => ['text' => $studentEssay],
]);
If the assignment accepts files:
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" }],
});
$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',
    ]],
]);

3. Trigger AI grading

await hk.submissions.gradeWithAi(submission.id);
$hk->submissions->gradeWithAi($submission->id);
This is async. Subscribe to the submission.graded webhook for production, or poll for prototypes:
const graded = await hk.poll(
  () => hk.submissions.retrieve(submission.id),
  { until: s => s.ai_status === "ready" || s.ai_status === "error" },
);
$graded = $hk->poll(
    fn () => $hk->submissions->retrieve($submission->id),
    ['until' => fn ($s) => $s->ai_status === 'ready' || $s->ai_status === 'error'],
);

4. Show the teacher

console.log(graded.grade_ai);             // 8.5
console.log(graded.grading_rationale);    // markdown breakdown
console.log(graded.grading_breakdown);    // [{criterion, score, max}, ...]
echo $graded->grade_ai;                   // 8.5
echo $graded->grading_rationale;          // markdown breakdown
print_r($graded->grading_breakdown);      // [{criterion, score, max}, ...]
Render grading_rationale as markdown next to the student’s text. The teacher gets context, not a black box.

5. Teacher confirms or overrides

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",
});
$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',
]);
If the teacher accepts the AI’s grade verbatim:
await hk.submissions.update(submission.id, {
  grade_manual: graded.grade_ai,
  human_review_status: "reviewed",
});
$hk->submissions->update($submission->id, [
    'grade_manual' => $graded->grade_ai,
    'human_review_status' => 'reviewed',
]);
Or, if you want to skip the override and trust the AI:
await hk.submissions.update(submission.id, { human_review_status: "reviewed" });
// `grade_manual` stays null; the gradebook reads from `grade_ai`.
$hk->submissions->update($submission->id, ['human_review_status' => 'reviewed']);
// `grade_manual` stays null; the gradebook reads from `grade_ai`.

6. Read the final grade

The SDK abstracts the precedence rule:
const sub = await hk.submissions.retrieve(submission.id);
console.log(sub.final_grade);   // grade_manual ?? grade_ai
$sub = $hk->submissions->retrieve($submission->id);
echo $sub->final_grade;   // grade_manual ?? grade_ai
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

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));
$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;
});

Tips

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.
Show the rubric to the student before they submit. A 30% increase in rubric-aligned answers is typical.
Never auto-publish AI grades on consequential assessments. The human_review: "required" flag exists for a reason. Use it.