Skip to main content
An Assignment is something you grade. A Submission is a single student’s response to it. Submissions hold both the response and the grade, because in our experience the two need to live next to each other: AI grading, human grading, regrades, comments, files.
Assignment "Essay: explain Lorentz invariance"
  ├─ Submission (student A)  → grade 8.5 (AI), 9.0 (human override)
  ├─ Submission (student B)  → grade 6.5 (AI)
  └─ Submission (student C)  → grade 0   (not submitted yet)

Creating an assignment

await hk.assignments.create({
  lesson_id: "lsn_123",
  type: "essay",
  title: "Explain Lorentz invariance in your own words",
  due_at: "2026-06-01T23:59:00Z",
  rubric: {
    criteria: [
      { name: "clarity", weight: 0.4 },
      { name: "physics accuracy", weight: 0.4 },
      { name: "use of sources", weight: 0.2 },
    ],
    scale: { min: 0, max: 10 },
  },
});
$hk->assignments->create([
    'lesson_id' => 'lsn_123',
    'type' => 'essay',
    'title' => 'Explain Lorentz invariance in your own words',
    'due_at' => '2026-06-01T23:59:00Z',
    'rubric' => [
        'criteria' => [
            ['name' => 'clarity', 'weight' => 0.4],
            ['name' => 'physics accuracy', 'weight' => 0.4],
            ['name' => 'use of sources', 'weight' => 0.2],
        ],
        'scale' => ['min' => 0, 'max' => 10],
    ],
]);
You can also configure the AI grader explicitly:
await hk.assignments.configure(assignmentId, {
  ai_evaluator: { enabled: true, model: "claude-sonnet-4-6" },
  human_review: "required" | "optional" | "off",
});
$hk->assignments->configure($assignmentId, [
    'ai_evaluator' => ['enabled' => true, 'model' => 'claude-sonnet-4-6'],
    'human_review' => 'required',     // 'required' | 'optional' | 'off'
]);

Submitting

A student submits with submissions.create():
await hk.submissions.create({
  assignment_id: "asg_123",
  student_id: "usr_123",
  content: { text: "Lorentz invariance means that the laws of physics ..." },
});
$hk->submissions->create([
    'assignment_id' => 'asg_123',
    'student_id' => 'usr_123',
    'content' => ['text' => 'Lorentz invariance means that the laws of physics ...'],
]);
You can attach files via multipart upload.

Grading

Three flavours, in this order of trust:

AI grading

await hk.submissions.gradeWithAi(submissionId);

const sub = await hk.submissions.retrieve(submissionId);
// sub.grade_ai             → 8.5
// sub.grading_rationale    → markdown explanation, criterion-by-criterion
$hk->submissions->gradeWithAi($submissionId);

$sub = $hk->submissions->retrieve($submissionId);
// $sub->grade_ai             → 8.5
// $sub->grading_rationale    → markdown explanation, criterion-by-criterion

Human override

await hk.submissions.update(submissionId, {
  grade_manual: 9.0,
  grader_comments: "Strong on conceptual clarity; missed the bit on simultaneity.",
});
$hk->submissions->update($submissionId, [
    'grade_manual' => 9.0,
    'grader_comments' => 'Strong on conceptual clarity; missed the bit on simultaneity.',
]);
The human grade always wins where present; AI grades stay attached for audit.

Bulk grading

When you want to grade an entire assignment at once (e.g. reading the whole class’ submissions overnight):
await hk.submissions.bulk({
  assignment_id: "asg_123",
  rows: classSubmissions.map(s => ({
    submission_id: s.id,
    grade_manual: s.draftScore,
    grader_comments: s.feedback,
  })),
});
$hk->submissions->bulk([
    'assignment_id' => 'asg_123',
    'rows' => array_map(fn ($s) => [
        'submission_id' => $s->id,
        'grade_manual' => $s->draftScore,
        'grader_comments' => $s->feedback,
    ], $classSubmissions),
]);

Human-in-the-loop pattern

A common pattern: AI proposes, human decides.
// 1. AI grades all of them
for (const sub of submissions) {
  await hk.submissions.gradeWithAi(sub.id);
}

// 2. Show teacher the AI grade + rationale; teacher confirms or edits
//    in your UI

// 3. Save with a manual grade (optional) and mark reviewed
await hk.submissions.update(submissionId, {
  grade_manual: teacherDecision.score,
  grader_comments: teacherDecision.notes,
  human_review_status: "reviewed",
});
// 1. AI grades all of them
foreach ($submissions as $sub) {
    $hk->submissions->gradeWithAi($sub->id);
}

// 2. Show teacher the AI grade + rationale; teacher confirms or edits
//    in your UI

// 3. Save with a manual grade (optional) and mark reviewed
$hk->submissions->update($submissionId, [
    'grade_manual' => $teacherDecision->score,
    'grader_comments' => $teacherDecision->notes,
    'human_review_status' => 'reviewed',
]);
This is the workflow we recommend for everything other than purely formative quizzes. It’s fast, it’s auditable, and it keeps a teacher on the hook for the final mark.

What Submission includes

type Submission = {
  id: string;
  assignment_id: string;
  student_id: string;
  content: SubmissionContent;
  files: File[];

  grade_ai: number | null;
  grade_manual: number | null;
  grading_rationale: string | null;     // markdown, AI-generated
  grader_comments: string | null;       // markdown, human-written

  human_review_status: "pending" | "reviewed" | "off";
  ai_status: "pending" | "ready" | "error";
  evaluated_at: string | null;          // ISO 8601
  uploaded_at: string;                  // ISO 8601
};