DEVELOPER DOCUMENTATION
Quiz Lifecycle Events and Webhooks
Nine typed project_todo.quiz.* events for assignment, reminders, submissions, grading, release, pass/fail and To-do completion, pushed by webhooks and served by GET /updates.
/api/public/v1/updatesEvery change to a quiz To-do produces one typed event. A webhook subscription pushes it to your endpoint, and GET /updates serves the same record. Your agent can follow a quiz from assignment to completion without opening OpenTrain.
Requirements: project_todos:read to see these events. To subscribe a webhook you also need webhooks:manage and the webhooks feature.
Event types
Section titled “Event types”type | When it happens | Extra data fields |
|---|---|---|
project_todo.quiz.assigned | A quiz To-do is assigned and bound to its published version. Once per assignment. | completionPolicy |
project_todo.quiz.reminder_due | An open quiz To-do needs a reminder: its deadline is close, it is overdue, or a required To-do without a deadline is still unopened. Once per reminder step. | reminderKind, dueAt, cadenceBucket |
project_todo.quiz.attempt_submitted | The AI trainer submits an attempt. Once per attempt. | attemptId, attemptNumber, attemptKind, reviewCycleId, gradeState, resultReleased |
project_todo.quiz.grading_required | A submitted attempt has manually graded questions that wait for your grade. Once per attempt. | attemptId, attemptNumber, pendingQuestionCount |
project_todo.quiz.graded | A grade is saved. PARTIAL leaves questions ungraded; FINALIZED grades all of them. Once per grade revision. | attemptId, attemptNumber, revisionNumber, gradeState, outcome, afterRelease |
project_todo.quiz.result_released | The result becomes visible to the AI trainer, either automatically or through your release. Once per attempt. | attemptId, attemptNumber, releaseMode, releasedAt |
project_todo.quiz.passed | The attempt passes. This fires at submit for an automatically graded quiz, or on the grade that first decides the result or changes it. | attemptId, attemptNumber, revisionNumber, previousOutcome |
project_todo.quiz.failed | The attempt fails. Same timing as passed. | attemptId, attemptNumber, revisionNumber, previousOutcome |
project_todo.quiz.todo_completed | The quiz To-do is completed: by the submission, by a passing grade, or by your approval. | completedVia, attemptId |
Every event’s data also carries assignmentId (the same value as the event’s resourceId), bindingId, formId and formVersionId. jobId on the event envelope is the job.
passed and failed only fire for quizzes with a pass rule. A new grade that keeps the same result sends graded but no second passed or failed. revisionNumber is null when the automatic grade at submit decided the result.
Field values
Section titled “Field values”| Field | Values |
|---|---|
completionPolicy | ON_SUBMIT, ON_REVIEW, ON_PASS |
reminderKind | DUE_SOON, OVERDUE, UNVIEWED (dueAt is null for UNVIEWED) |
attemptKind | FULL, or REMEDIATION for a selective coaching revision (reviewCycleId is set) |
gradeState on attempt_submitted | GRADED, PENDING_MANUAL |
gradeState on graded | PARTIAL, FINALIZED |
outcome, previousOutcome | PASSED, FAILED, or null (not decided, or no pass rule) |
releaseMode | IMMEDIATE, MANUAL |
completedVia | SUBMISSION, PASS, EMPLOYER_APPROVAL (attemptId is null for an approval) |
What payloads never contain
Section titled “What payloads never contain”Payloads hold IDs and states only. They never contain answers, answer keys, rubrics, points, grading notes, names or email addresses. To read the details of an attempt, call GET /quizzes/attempts/{attemptId} with the attemptId from the event.
No duplicates on retries
Section titled “No duplicates on retries”OpenTrain writes each event in the same database transaction as the change it describes, keyed on that change. So:
- A retried or concurrent submit, grade, release or approval produces exactly one event per change.
- An event is never lost while its change is saved. If the event cannot be stored, the change is rolled back.
Webhook deliveries can still repeat after a network failure. Dedupe on the X-OpenTrain-Delivery header or on the event id.
Subscribe
Section titled “Subscribe”curl -X POST "https://www.opentrain.ai/api/public/v1/webhooks" \ -H "Authorization: Bearer $OPENTRAIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/opentrain-webhook", "eventTypes": [ "project_todo.quiz.attempt_submitted", "project_todo.quiz.grading_required", "project_todo.quiz.todo_completed" ] }'opentrain webhooks create --url https://example.com/opentrain-webhook \ --events project_todo.quiz.attempt_submitted,project_todo.quiz.grading_required,project_todo.quiz.todo_completed --jsonIn MCP, call the webhook create tool with the same eventTypes. The quiz capabilities document (GET /project-todos/quizzes/authoring/capabilities, opentrain todos quiz capabilities, or the opentrain_quiz_authoring_capabilities tool) lists all nine types under workflows.events.
Example event
Section titled “Example event”{ "id": "981245", "type": "project_todo.quiz.graded", "apiVersion": "v1", "createdAt": "2026-11-02T10:15:04.000Z", "resourceId": "assignment_123", "jobId": "job_456", "data": { "assignmentId": "assignment_123", "bindingId": "binding_789", "formId": "form_321", "formVersionId": "version_654", "attemptId": "attempt_987", "attemptNumber": 1, "revisionNumber": 2, "gradeState": "FINALIZED", "outcome": "PASSED", "afterRelease": false }}Selective coaching has its own project_todo.quiz.coaching.* events. See coaching.