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