DEVELOPER DOCUMENTATION
Screen applicants with a quiz
Add an application screening quiz to a job with the CLI or MCP: create a course with a quiz, link it with stage SCREENING before publishing, review and grade each applicant, rank by screening score, and backfill earlier proposals.
An application screening quiz is an LMS course with a graded quiz that every applicant to a job completes as part of their application. You link a published course to the job with the SCREENING stage. From then on, every new proposal on the job is enrolled automatically: the AI trainer submits the proposal, then completes the course and quiz. You review each enrollment by proposal, rank proposals by screening score, and still make the hiring decision yourself.
This is different from quizzes assigned as Project To-dos, which go to AI trainers you have already hired. For the employer view of the same feature, see Application screening quiz.
Before you start
Section titled “Before you start”- Versions:
@opentrain-ai/clior@opentrain-ai/mcp0.27.0 or later. Per-question applicant results (Step 7) need 0.30.0 or later. The hosted MCP endpoint athttps://app.opentrain.ai/mcpexposes the same tools. - Permissions:
lms:readandlms:writefor courses, the screening link, status, and review;proposals:readto list and rank proposals;jobs:writeto publish the job. See Scopes and Capabilities. - Discovery: call
opentrain lms capabilities --json(MCP:opentrain_lms_capabilities) for the current question types, settings enums, and limits instead of relying on remembered values. - A job draft: create one with the Post a job flow, and stop before Step 3 (Publish) of that guide.
The full sequence:
1. Create and publish the quiz lms assessments create / publish2. Put it in a course and publish it lms courses create → checkout → push → publish3. Link the course with stage SCREENING lms training link --stage SCREENING --max-attempts <n>4. Publish the job jobs publish5. Watch enrollment lms screening status6. Review, grade, release, decide lms review / grade / release-result / decide --proposal7. Rank applicants proposals list --sort screening-score8. Backfill proposals that predate the link lms screening assignPlaceholders such as <job-id>, <course-id>, <assessment-id>, and <proposal-id> stand for IDs returned by earlier calls. Idempotency keys in the examples are illustrative; use your own unique key per operation.
Step 1: Create and publish the quiz
Section titled “Step 1: Create and publish the quiz”A quiz is a Native Forms assessment. Save a definition like this as screening-quiz.json:
{ "title": "Applicant screening quiz", "definition": { "schemaVersion": 2, "questions": [ { "id": "q1", "type": "MULTIPLE_CHOICE", "prompt": "Which sentiment label fits a sentence that states a fact with no opinion?", "required": true, "options": [ { "id": "a", "label": "Positive" }, { "id": "b", "label": "Neutral" }, { "id": "c", "label": "Negative" } ], "grading": { "points": 1, "answerKey": { "optionId": "b" } } }, { "id": "q2", "type": "PARAGRAPH", "prompt": "Explain how you would label a sarcastic product review.", "required": true, "manualGrading": { "points": 4 } } ] }, "settings": { "attemptPolicy": { "mode": "LIMITED", "maxAttempts": 2 }, "passRule": { "mode": "PERCENTAGE", "thresholdPercent": 70 }, "resultRelease": "IMMEDIATE" }}CLI
opentrain lms assessments create \ --input screening-quiz.json \ --key screening-quiz-create-01
opentrain lms assessments publish \ --assessment <assessment-id> \ --expected-revision <draft-revision> \ --confirm-publish \ --key screening-quiz-publish-01Run opentrain lms assessments create --help for every question type and grading option.
MCP
Call opentrain_lms_manage_assessment:
{ "operation": { "action": "create", "title": "Applicant screening quiz", "definition": { "schemaVersion": 2, "questions": ["...as above..."] }, "settings": { "attemptPolicy": { "mode": "LIMITED", "maxAttempts": 2 }, "passRule": { "mode": "PERCENTAGE", "thresholdPercent": 70 }, "resultRelease": "IMMEDIATE" }, "idempotencyKey": "screening-quiz-create-01" }}Then call opentrain_lms_publish_assessment:
{ "formId": "<assessment-id>", "expectedDraftRevision": <draft-revision>, "confirmPublish": true, "idempotencyKey": "screening-quiz-publish-01"}Use the integer draft revision returned by the create call or by opentrain_lms_get_assessment.
How the quiz settings behave for applicants:
attemptPolicy: the link’s attempt ceiling (Step 3) also applies. Applicants get the lower of the two; anUNLIMITEDquiz gets the ceiling.passRule: required for a pass or fail result (passed). Without it, a fully graded attempt has a percentage but no pass or fail.resultRelease:IMMEDIATEshows the applicant their score once the attempt is graded.MANUALholds the score until you release it in Step 6.manualGradingquestions: the score stays pending until you grade them in Step 6. Proposals awaiting grades rank below fully graded ones.
You can also build and publish the quiz in Forms in the OpenTrain app; the published form works the same way.
Step 2: Put the quiz in a course and publish it
Section titled “Step 2: Put the quiz in a course and publish it”The course needs an ASSESSMENT lesson that references the published quiz. Lessons before the quiz, such as a project overview, are optional. A module with only the quiz looks like this:
{ "key": "screening", "title": "Screening", "lessons": [ { "key": "screening-quiz", "title": "Screening quiz", "kind": "ASSESSMENT", "required": true, "assessment": { "formId": "<assessment-id>", "passRequired": true, "manualReview": false } } ]}With passRequired: true, an applicant who fails can retake the quiz while attempts remain. Set manualReview: true only if you want to record the final pass or fail yourself with decide (Step 6).
CLI
opentrain lms courses create --title "Applicant screening" --key screening-course-create-01opentrain lms checkout --course <course-id> --dir ./screening-courseSave the module above as ./screening-course/modules/01-screening.json and add that path to moduleFiles in ./screening-course/course.yaml. Then validate, push, and publish:
opentrain lms validate --dir ./screening-course --remote --key screening-course-validate-01opentrain lms push --dir ./screening-courseopentrain lms publish --dir ./screening-course --confirm-publish --key screening-course-publish-01MCP
Call opentrain_lms_manage_course with the module in draftTree:
{ "operation": { "action": "create", "title": "Applicant screening", "draftTree": { "schemaVersion": 1, "modules": ["...the module above..."] }, "idempotencyKey": "screening-course-create-01" }}Then call opentrain_lms_validate_course with { "courseId": "<course-id>", "idempotencyKey": "screening-course-validate-01" } and publish with opentrain_lms_publish_course:
{ "courseId": "<course-id>", "expectedDraftRevision": <draft-revision>, "confirmPublish": true, "idempotencyKey": "screening-course-publish-01"}Use the integer draft revision returned by the create call or by opentrain_lms_get_course.
If validation returns warnings, publishing requires acknowledging them. See Validate LMS authoring before publishing. An employer can also build the course under Training in the OpenTrain app; link it in Step 3 the same way.
Step 3: Link the course to the job as its screening course
Section titled “Step 3: Link the course to the job as its screening course”CLI
opentrain lms training link \ --job <job-id> \ --course <course-id> \ --stage SCREENING \ --max-attempts 2 \ --confirm-link \ --key screening-link-01MCP
Call opentrain_lms_link_job_course:
{ "jobId": "<job-id>", "courseId": "<course-id>", "stage": "SCREENING", "screeningMaxAttempts": 2, "confirmLink": true, "idempotencyKey": "screening-link-01"}Rules for a screening link:
- The course must have a published version when you link it.
- A job has at most one active screening link.
--max-attempts(MCP:screeningMaxAttempts) is an integer from 1 to 100 and defaults to 1. It applies only with theSCREENINGstage.- Linking enrolls nobody who has already applied. It does not need the job’s Training setting and turns no job settings on.
- Without
--stage, a link that already exists keeps its stage and ceiling. The stage and ceiling are part of the idempotency receipt, so retrying the same key with different values returns409. - Linking from a job’s Training tab in the OpenTrain app creates a post-hire training link, not a screening link.
Confirm the link before you publish:
opentrain lms screening status --job <job-id> --prettyMake the quiz the only screening step
Section titled “Make the quiz the only screening step”A screening quiz does not replace the AI interview. By default the AI interview is required, so applicants take both. To screen with the quiz alone, turn the interview requirement off:
CLI
opentrain jobs interview set --job-id <job-id> --not-requiredMCP
On a draft, call opentrain_update_job_draft_fields with { "jobId": "<job-id>", "patch": { "aiInterviewRequired": false } }. On a live job, use opentrain_update_published_job with the same patch.
Step 4: Publish the job
Section titled “Step 4: Publish the job”Publish as usual with opentrain jobs publish --job-id <job-id> (MCP: opentrain_publish_job). See Post a job. Every proposal submitted from now on is enrolled in the screening course.
Step 5: Watch enrollment
Section titled “Step 5: Watch enrollment”CLI
opentrain lms screening status --job <job-id> --limit 500MCP
Call opentrain_lms_screening_status with { "jobId": "<job-id>", "limit": 500 }.
The response describes the link and every proposal on the job, newest first:
| Field | Meaning |
|---|---|
screening.link | The active screening course, its attempt ceiling (maxAttempts), and its current published version. null when the job has no screening link. |
screening.proposals[].enrollmentKey | proposal:<proposal-id>. Pass it as the assignment ID to the review tools in Step 6. |
screening.proposals[].binding.status | OPEN (not started), IN_PROGRESS, or SUBMITTED. |
screening.proposals[].binding.waitingOn | WORKER (the applicant’s move), EMPLOYER (a manual grade or decision is pending), or NONE. |
screening.proposals[].binding.courseVersionCurrent | false when the applicant is on an older course version. |
screening.proposals[].binding = null | A binding gap: this proposal is not enrolled. |
screening.bindingGaps | How many proposals are not enrolled. Backfill them in Step 8. |
screening.staleVersionCount | How many applicants are on an older course version, so their scores may not compare directly. |
screening.hasMore | true when the job has more proposals than limit (default and maximum 500). |
Step 6: Review, grade, release, and decide each applicant
Section titled “Step 6: Review, grade, release, and decide each applicant”The review commands are the same ones used for post-hire training. Address a screening enrollment with --proposal <proposal-id> instead of --assignment <id>. In MCP, pass the enrollment key proposal:<proposal-id> as assignmentId.
CLI
# Read submitted answers, grades, and each lesson's review targetopentrain lms review --proposal <proposal-id>
# Grade manually graded questionsopentrain lms grade --proposal <proposal-id> \ --lesson screening-quiz --attempt <attempt-id> \ --grades-json '[{"questionId":"q2","awardedPoints":3,"reason":"Covers tone, misses context."}]' \ --expected-target <target-hash> \ --key screening-grade-01
# Make a held (MANUAL release) result visible to the applicantopentrain lms release-result --proposal <proposal-id> \ --lesson screening-quiz --attempt <attempt-id> \ --confirm-release --expected-target <target-hash> \ --key screening-release-01
# Record a final verdict on a manual-review lessonopentrain lms decide --proposal <proposal-id> \ --lesson screening-quiz --decision PASS \ --confirm-decision --expected-target <target-hash> \ --key screening-decide-01opentrain lms attempts list --proposal <proposal-id> lists every attempt; opentrain lms attempts get --proposal <proposal-id> --attempt <attempt-id> shows per-question evidence for one attempt.
MCP
opentrain_lms_review_assignmentwith{ "assignmentId": "proposal:<proposal-id>" }.opentrain_lms_grade_assessmentwithassignmentId,lessonKey,attemptId,grades,expectedTargetHash, andidempotencyKey.opentrain_lms_release_resultwithassignmentId,lessonKey,attemptId,confirmRelease: true,expectedTargetHash, andidempotencyKey.opentrain_lms_decide_assessmentwithassignmentId,lessonKey,decision(PASSorFAIL),confirmDecision: true,expectedTargetHash, andidempotencyKey.
Order and safety:
- Grade first, then release, then decide. Release is blocked while manual grades are outstanding.
reviewreturns areviewTargetwith atargetHashfor each lesson. Pass it as--expected-target(MCP:expectedTargetHash). If the applicant resubmitted after you reviewed, the call fails with409andREVIEW_TARGET_STALEinstead of acting on a different attempt.- Releasing reveals the score, per-question detail, and grading notes to the applicant.
- Decisions are final.
FAILrequires--reason(MCP:reason), which the applicant sees as feedback. - None of these calls hires or declines anyone. The hiring decision stays on the proposal.
Step 7: Rank applicants by screening score
Section titled “Step 7: Rank applicants by screening score”CLI
opentrain proposals list --job-id <job-id> --sort screening-score --limit 100MCP
Call opentrain_list_proposals:
{ "jobId": "<job-id>", "sort": "screening_score", "limit": 100 }The REST equivalent is GET /api/public/v1/jobs/{id}/proposals?sort=screening_score. How the ranking works:
- Order: highest finalized percentage first, then fully graded attempts without a percentage, then submissions awaiting grades, then enrollments not yet submitted, then proposals with no enrollment.
- One request returns one ranked page of up to
limitproposals (default 25, maximum 100). There is no cursor with this sort. - The ranking covers the newest 500 proposals on the job.
ranking.rankedCountis how many proposals it covered,ranking.scoredCountis how many have a finalized percentage, andranking.completeisfalsewhen the job has more proposals than the ranking covers.hasMoreistruewhen more ranked rows exist thanlimit, or when the ranking is incomplete. - If screening scores cannot be read, the request fails with a retryable
503rather than returning a recency order. Retry, or usesort=recent, where each row then reportsscreeningRead: "FAILED".
Every proposal row, and proposals get (MCP: opentrain_get_proposal), carries a screening block (enrollmentKey, status, waitingOn, courseVersionNumber, courseVersionCurrent, gradeState, earnedPoints, possiblePoints, percentage, passed, submittedAt, released) and screeningRead (OK or FAILED). screening is null when the job has no screening course or the proposal is not enrolled. When screeningRead is FAILED, a null block means unknown, not “not enrolled”. percentage and passed appear only once every submitted quiz is fully graded.
To see which questions applicants miss, read per-question results for the applicant audience. Names are masked as first name and last initial.
CLI
opentrain lms question-results --job <job-id> --audience APPLICANT --course <course-id>MCP
Call opentrain_lms_question_results with { "jobId": "<job-id>", "audience": "APPLICANT", "courseId": "<course-id>" }.
Step 8: Backfill proposals submitted before the link
Section titled “Step 8: Backfill proposals submitted before the link”If the job was published before the screening link, or an enrollment did not complete when a proposal was submitted, the proposal has no enrollment. Find them in Step 5 (binding: null, counted by bindingGaps), then enroll each one:
CLI
opentrain lms screening assign \ --job <job-id> \ --proposal <proposal-id> \ --confirm-assign \ --key screening-assign-<proposal-id>MCP
Call opentrain_lms_screening_assign:
{ "jobId": "<job-id>", "jobOfferId": "<proposal-id>", "confirmAssign": true, "idempotencyKey": "screening-assign-<proposal-id>"}The backfilled enrollment is identical to one created at submission and uses the link’s current published course version. The call is idempotent per proposal: created: false means the proposal was already enrolled, and retrying the same key replays the receipt. Typed refusals:
| Status | Code | Meaning |
|---|---|---|
409 | SCREENING_NOT_CONFIGURED | The job has no active screening link. |
409 | SCREENING_COURSE_UNPUBLISHED | The linked course has no published version. |
400 | SCREENING_BINDING_INVALID | The ID is not a proposal on this job, or it is an invite rather than a proposal. |
Change or remove the screening course
Section titled “Change or remove the screening course”- New course version: publish the course again. New enrollments, including backfills, use the new version. Applicants already enrolled keep their version and count toward
staleVersionCount. - Stop screening: run
opentrain lms training unlink --job <job-id> --course <course-id> --confirm-unlink --key <key>(MCP:opentrain_lms_unlink_job_course). The link is retired with its history kept. Applicants who have not finished then see that the screening course is no longer available.
REST endpoints
Section titled “REST endpoints”| Method | Endpoint | Purpose |
|---|---|---|
POST | /api/public/v1/lms/assessments | Create the quiz draft. |
POST | /api/public/v1/lms/assessments/{formId}/publish | Publish the quiz. |
POST | /api/public/v1/lms/courses | Create the course draft. |
POST | /api/public/v1/lms/courses/{courseId}/publish | Publish the course. |
POST | /api/public/v1/lms/jobs/{jobId}/courses/link | Link the course with stage: "SCREENING" and screeningMaxAttempts. |
GET | /api/public/v1/lms/jobs/{jobId}/screening | Read the link and every proposal’s enrollment. |
POST | /api/public/v1/lms/jobs/{jobId}/screening/assign | Backfill one proposal (jobOfferId, confirmAssign: true). |
GET | /api/public/v1/lms/assignments/{assignmentId}/review | Review one enrollment; assignmentId is proposal:<proposal-id>. |
POST | /api/public/v1/lms/assignments/{assignmentId}/grades | Record manual grades. |
POST | /api/public/v1/lms/assignments/{assignmentId}/release-result | Release a held result. |
POST | /api/public/v1/lms/assignments/{assignmentId}/decision | Record a final pass or fail. |
GET | /api/public/v1/jobs/{id}/proposals?sort=screening_score | Rank proposals by screening score. |
GET | /api/public/v1/lms/jobs/{jobId}/question-results?audience=APPLICANT | Per-question applicant results. |
Every write requires an Idempotency-Key header. See List Job Proposals for the ranking response fields.
Completion checklist
Section titled “Completion checklist”Before you report that screening is set up:
- the human agreed to screen applicants with a quiz, and the course has a published version;
lms screening statusshows the link, with the attempt ceiling you intended;- the AI interview requirement matches what the human wants (both screens, or the quiz alone);
- the link was created before publishing, or
bindingGapsis0after backfilling earlier proposals; - you shared how to review results: the Screening quiz column in the job’s Proposals tab, or a ranked
proposals list --sort screening-score.
Related
Section titled “Related”The employer view: what applicants see and how results appear in proposal review.
Draft and publish the job; link the screening course before Step 3.
Combine screening scores with interview scores, profiles, and pre-hire messages.
Assign quizzes to AI trainers you have already hired.