DEVELOPER DOCUMENTATION
Roll Out a Quiz (Plan, Then Confirm)
Publish a quiz draft and assign it as a Project To-do in one reviewed decision: a no-write plan with a signed token, then one idempotent confirm.
/api/public/v1/project-todos/quizzes/manageA rollout publishes a quiz and assigns it to AI trainers as one decision that a person reviews once. It has two steps on the shared manage endpoint:
quiz_rollout_planwrites nothing. It returns every fact the person needs to approve: the exact version that will be published, the To-do settings with the projected due and blocking times, the masked list of current recipients, active duplicates, the notification impact, and a signedplan.token.quiz_rollout_confirmtakes that token. It publishes the draft (or binds the version you planned), then creates and assigns the Project To-do, all under one idempotency key.
The person’s “yes” in the agent conversation or terminal is the confirmation. No OpenTrain page is required.
quiz_rollout_plan needs only project_todos:read and no idempotency key. For a draft grounded in the job’s Instructions it also needs instructions:read, because it reads the cited pages. It changes nothing users see. For a grounded draft, resolving the released Instructions may record the same idempotent view row that an AI trainer’s read records.
actionstringbodyrequiredquiz_rollout_planjobIdstringbodyrequiredformIdstringbodyrequiredsourceobjectbodyrequired{ "kind": "DRAFT", "expectedDraftRevision": n } plans to publish the draft at the revision you reviewed. { "kind": "VERSION", "formVersionId": "…" } plans to bind a version that is already published, without publishing again.
todoobjectbodyrequiredThe Project To-do settings: completionPolicy (ON_SUBMIT, ON_PASS, or ON_REVIEW), audience (ACTIVE, FUTURE, ACTIVE_AND_FUTURE, or SELECTED with up to 500 selectedContractIds), and optional priority (NORMAL, HIGH, URGENT), blockingMode (NONE, IMMEDIATE, AFTER_DUE), dueOffsetMinutes (1 to 525,600), required, title, description, and categoryId.
REST
curl -s -X POST "https://www.opentrain.ai/api/public/v1/project-todos/quizzes/manage" \ -H "Authorization: Bearer $OPENTRAIN_API_TOKEN" -H 'content-type: application/json' -d '{ "action": "quiz_rollout_plan", "jobId": "job_123", "formId": "form_456", "source": { "kind": "DRAFT", "expectedDraftRevision": 3 }, "todo": { "completionPolicy": "ON_PASS", "audience": "ACTIVE_AND_FUTURE", "priority": "HIGH", "blockingMode": "AFTER_DUE", "dueOffsetMinutes": 1440 }}'SDK
import { OpenTrainClient } from '@opentrain-ai/sdk';
const opentrain = new OpenTrainClient({ apiToken: process.env.OPENTRAIN_API_TOKEN!,});
const todo = { completionPolicy: 'ON_PASS', audience: 'ACTIVE_AND_FUTURE', priority: 'HIGH', blockingMode: 'AFTER_DUE', dueOffsetMinutes: 1440,} as const;
const plan = await opentrain.planProjectTodoQuizRollout({ jobId: 'job_123', formId: 'form_456', source: { kind: 'DRAFT', expectedDraftRevision: 3 }, todo,});CLI
opentrain todos quiz rollout plan --job-id job_123 --form-id form_456 --draft-revision 3 \ --completion-policy ON_PASS --audience ACTIVE_AND_FUTURE --priority HIGH \ --blocking-mode AFTER_DUE --due-offset-minutes 1440Use --form-version-id <id> instead of --draft-revision to plan a version that is already published.
MCP
Call opentrain_plan_quiz_rollout with the same jobId, formId, source, and todo fields.
What the plan returns
Section titled “What the plan returns”| Field | Meaning |
|---|---|
publishable | true when nothing blocks. Only then is plan set. |
blockers[] | Each blocker has a code and a message. Codes: PROJECT_TODOS_DISABLED, FORM_ARCHIVED, DRAFT_REVISION_STALE, DRAFT_INVALID, NOT_PUBLISHABLE, ASSET_UNUSABLE, SOURCE_CHANGED, GROUNDING_DRAFT_PREVIEW, LEARNING_PLAN_MISMATCH, ON_PASS_REQUIRES_PASS_RULE, ON_PASS_REQUIRES_AUTO_GRADING. |
warnings[] | Facts to show the person that do not block. |
form | The source (DRAFT or VERSION), draftRevision, nextVersionNumber, and contentHash of what will be published. |
publication | Total points, answerable and manually graded question counts, the pass rule, the attempt policy, and the result release mode. |
todo | The settings as they will be stored, with projectedDueAt and projectedBlockAt. |
audience | Whether the audience is broad, whether it includes future hires, currentRecipientCount, and recipients (contract id plus masked display name). |
duplicates | Active to-dos on the same form (sameForm) or the same version (sameFormVersion). |
notifications | How many assignment notifications go out, whether future hires get the to-do automatically, and whether it blocks project work. |
plan | { token, expiresAt }. The token expires after 15 minutes and is bound to the actor, job, form, draft revision and content hash (or version), the To-do settings, and the recipient set. |
nextAction | One sentence that says what to do next. |
Show the plan to the person before you confirm.
Confirm
Section titled “Confirm”quiz_rollout_confirm needs project_todos:write, a claimed account, and a stable idempotencyKey. For a grounded draft it also needs instructions:read.
actionstringbodyrequiredquiz_rollout_confirmidempotencyKeystringbodyrequired., _, :, or -. Keep it stable across retries.planTokenstringbodyrequiredplan.token from the plan.todoobjectbodyrequiredThe exact same todo you planned. The token signs a digest of it, not its contents, so you send it again.
allowDuplicatebooleanbodytrue to assign the exact published version again when the plan listed it under duplicates.sameFormVersion. Without it that request is refused. An earlier version of the same form (sameForm) only adds a warning: confirming adds a new to-do next to the old one.REST
curl -s -X POST "https://www.opentrain.ai/api/public/v1/project-todos/quizzes/manage" \ -H "Authorization: Bearer $OPENTRAIN_API_TOKEN" -H 'content-type: application/json' -d '{ "action": "quiz_rollout_confirm", "idempotencyKey": "safety-quiz-rollout-2026-11", "planToken": "PLAN_TOKEN_FROM_THE_PLAN", "todo": { "completionPolicy": "ON_PASS", "audience": "ACTIVE_AND_FUTURE", "priority": "HIGH", "blockingMode": "AFTER_DUE", "dueOffsetMinutes": 1440 }}'SDK
if (plan.publishable && plan.plan) { const rollout = await opentrain.confirmProjectTodoQuizRollout({ idempotencyKey: 'safety-quiz-rollout-2026-11', planToken: plan.plan.token, todo, // the same object you planned });}CLI
opentrain todos quiz rollout confirm --plan-token PLAN_TOKEN_FROM_THE_PLAN \ --idempotency-key safety-quiz-rollout-2026-11 \ --completion-policy ON_PASS --audience ACTIVE_AND_FUTURE --priority HIGH \ --blocking-mode AFTER_DUE --due-offset-minutes 1440 --confirm-liveThe CLI requires --confirm-live, an explicit --idempotency-key, and the same To-do flags as the plan.
MCP
Call opentrain_confirm_quiz_rollout with planToken, the same todo, and a stable idempotencyKey.
The response has ok: true, published (formVersionId, versionNumber, createdNow, resumed), todo (itemId, versionId, replayed), the fan-out counts (matchedContracts, createdAssignments, skippedExistingAssignments, failedAssignments), warnings, and replayed.
Retries and drift
Section titled “Retries and drift”Before writing anything, confirm checks the roster and the Project To-dos setting again.
Status and details.reason | What happened | What to do |
|---|---|---|
409 PLAN_STALE | Something changed since the plan: the roster, the Project To-dos setting, the draft, the archive state, or the version content. details.changed says what. Nothing was written. | Plan again. |
409 PLAN_TODO_MISMATCH | The todo differs from the planned one. | Send the planned todo. |
409 PLAN_TOKEN_EXPIRED / PLAN_TOKEN_INVALID | The token is older than 15 minutes, or it belongs to another actor or plan. | Plan again. |
409 TARGET_ALREADY_MUTATED | This plan was already confirmed under a different key. Nothing is assigned twice. | Retry with the first key to get the result, or plan again for a new rollout. |
409 IDEMPOTENCY_KEY_REUSED | Another operation already used this key to create a to-do on the job. Refused before anything is published. | Use a new key. |
A retry with the same key returns the same version and to-do with replayed: true. The publication is recorded under rollout:<idempotencyKey>, so a confirm that lost its reply after publishing resumes that version on retry, even after the token would have expired. If the roster changes between the publication and the assignment, the 409 PLAN_STALE names the new version in details.published. Retry with the same key once the roster matches, or plan again with { "kind": "VERSION", "formVersionId": … } to bind it without publishing a duplicate.
Related
Section titled “Related”- Author forms and quizzes for single-step
form_publish. - Assign a quiz for
quiz_todo_preflightandquiz_todo_createon a version that is already published. - Ground a quiz in Instructions for the
SOURCE_CHANGEDblocker.