@opentrain-ai/cli
v0.41.0
Published
Agent-first command-line client for the OpenTrain public API: author and assign LMS retraining, author/publish adaptive job Task Checklists (file-first machine envelope), run employer-only Reviewer Feedback review/triage/access/settings, and manage jobs,
Readme
OpenTrain CLI
Command-line client for the OpenTrain public API.
Built for coding agents and scripts: register an agent account, draft and
publish jobs, review proposals, hire freelancers, manage contracts and
milestones, message, poll updates or register webhooks, and manage credits
and your team — all from the terminal. The same CLI also maintains the
canonical Instructions pages for a job, builds and assigns Project To-dos and
quizzes, runs the complete Training/LMS authoring and review lifecycle, and
drives DEV-2741 selective quiz coaching for employers:
todos quiz coaching … (queue/state/threads reads, draft feedback, redo
selection, accept, planned + confirmed send-back), plus the distinct FULL-retake
todos quiz remediate plan|apply lane. Every coaching subcommand answers
--help with its focused contract; every mutation demands a caller-supplied
stable --idempotency-key. Agents can also discover the live contract,
filter/export coaching work, and attach private feedback images entirely from
the CLI. todos quiz coaching image upload is the safe default; image
download --out streams, verifies, and atomically places a worker image so the
calling agent can inspect it without exposing a signed storage grant.
Freelancer API keys are deliberately read-only: they can search public jobs and read the owner's authorized messages. They cannot send messages, answer or submit quizzes, use coaching actions, or mutate account data.
Money never moves from the CLI. Ordinary hire, milestone funding/approval, and
contract-end requests record a pending approval for a signed-in human. The
separate default-off admin-balance-* family is available only after the
server independently verifies every canonical OpenTrain-admin gate; it writes
an internal funding marker with a $0 funding movement and never exposes payout
release, which remains human-authorized.
Batch hire approvals (default-off preview)
proposals hire-batch [--job-id <id>] --idempotency-key <stable-value> accepts
one of repeated --proposal-id, --proposal-ids <csv>, or --file <json>.
Use 1–20 unique applicants on one job. The file contains defaults and items
(with per-person milestone/rate overrides), never a confirmation or payer ID.
Without --job-id, an authorized proposal read resolves the job. Dates require
full ISO datetimes with timezone. See proposals hire-batch help for flags.
The response is one human review link, not a completed hire. A person reviews,
edits or deselects applicants and confirms the priced revision. Each selected
hire then executes independently; successes survive other failures. The initial
milestone uses the bid; a different requested rate needs worker acceptance.
Repeat uncertain requests with the same input/key. Read current per-item results
with proposals hire-batch get --batch-id <id>, including after new requests
are disabled. --pretty retains the full receipt; write commands refuse --fields
and conditional-read options. Existing one-ID proposals hire is unchanged.
Restricted admin-balance milestone cancellation (0.27.3)
Verified canonical OpenTrain platform admins can cancel an eligible internally funded milestone without moving money:
opentrain admin-balance-milestones cancel \
--contract-id <contract-id> --milestone-id <milestone-id> \
--idempotency-key <stable-key> [--notify-counterparty] [--pretty]The operation is request-idempotent and returns fundingDisposition=NO_MONEY_MOVED
and moneyMoved=false; it never creates a Stripe refund or releases a payout.
Notifications are suppressed unless --notify-counterparty is explicitly
provided. Card- and credit-funded milestones stay on the signed-in human
approval path and are refused by the server. This command requires CLI 0.27.3
and the matching serving API.
Reusable private video library
CLI 0.11+ exposes the rollout-gated opentrain videos family. Agents can list
and inspect organization-owned videos, upload a local video directly through a
one-time resumable grant, poll/cancel processing, record draft Instructions or
LMS placement references, prepare/download an MP4 rendition, and fetch playback
metadata. Run opentrain videos --help for the complete command list.
Video bytes never pass through OpenTrain JSON. videos download grant|save
requires an API token explicitly minted with videos:download. Recording a
reference does not insert a TipTap/LMS node or publish a worker-visible pin;
those are separate, atomic product mutations.
OpenTrain Agreements documents (rollout-gated)
The opentrain agreements family prepares organization-owned PDF agreements
for e-signature. It needs a token with agreements:read (reads) or
agreements:write (mutations) and the server-side Agreements rollout. Run
opentrain agreements help for the complete command list.
opentrain agreements documents create --title "Contractor NDA" --key contractor-nda
opentrain agreements documents upload --document-id <id> --file nda.pdf --expected-revision <n>
opentrain agreements fields set --document-id <id> --expected-revision <n> --fields-file fields.json
opentrain agreements preview --document-id <id> --pages 1,3 --out ./previews
opentrain agreements publish --document-id <id> --expected-revision <n> --confirm-liveExecuted agreements (both providers). agreements requests lists the
organization's signing requests: the native ones attach created
(native:<id>) and legacy Dropbox Sign envelopes (dropbox_sign:<id>,
provider: DROPBOX_SIGN). Each row says whether the exact executed PDF is
stored (executedDocument.available, SHA-256, size). To read what one worker
signed on a contract and save the executed PDF, hash-checked and audited:
opentrain agreements requests list --contract-id <contractId> --json
opentrain agreements requests get dropbox_sign:<attemptId> --json
opentrain agreements requests download dropbox_sign:<attemptId> --artifact signed --out ./ica-signed.pdfFor a whole job roster, page agreements requests list --job-id <jobId> and
download --artifact signed for every row whose executedDocument.available
is true. remind and audit take native:<id> requests only.
documents upload is the only upload command: it hashes the PDF locally,
reserves a one-time signed PUT, streams the bytes straight to private storage
and finalizes with the local SHA-256 so the server verifies what it stored.
The signed URL, its headers and storage paths never appear in human output,
the JSON envelope, or errors; re-run with the same --idempotency-key to
resume an interrupted upload. preview returns short-lived image handles;
--out <dir> downloads each page with your token to <dir>/page-<n>.png and
refuses to overwrite unless --force. publish freezes an immutable version
and requires --confirm-live. Every mutation sends an Idempotency-Key
(default cli-<uuid>, announced on stderr) and accepts --dry-run to print
the planned request without any network call. API failures keep the server's
details.reason (for example REVISION_CONFLICT, CHECKSUM_MISMATCH,
PUBLISH_WARNINGS_UNWAIVED).
Install
npm install -g @opentrain-ai/cli
opentrain --helpOr run without installing:
npx -y @opentrain-ai/cli@latest whoamiWhen running from a monorepo checkout that already has an
@opentrain-ai/cli workspace, include @latest so npm resolves the published
package instead of an unbuilt local workspace.
Updating while agents are running
opentrain version check reports whether a newer release exists and prints
the install command; the CLI never updates itself. npm install -g replaces
the package in place, so for a few seconds during an update the opentrain
command can be missing (opentrain: No such file or directory) for any
process that calls it. To keep long-running agent loops safe:
- Update between runs, not while an agent is mid-task. If a call fails with "No such file or directory" right after an update started, wait a few seconds and retry the same command.
- Or pin a version per project, which a global update never touches:
npm install --save-dev @opentrain-ai/cli@<version>and run it withnpx opentrain …, or callnpx -y @opentrain-ai/cli@<version> …directly.
Existing account: browser login (0.27.0+, rollout-gated)
Each browser-auth command has focused, offline --help (for example,
opentrain auth login --help and opentrain auth login wait --help). Help never
creates a pending login or contacts the server. During polling, temporary
503/429 responses honor server-directed backoff without creating a new login;
resume the same loginId after a wait timeout, until its reported expiry.
Browser-login commands are included in CLI 0.27.0+. The server migration and
agent_browser_login rollout must be delivered before login can succeed.
A disabled server refuses the request; do not create another account as a
workaround. Existing API-key login remains supported. Package publication does
not enable the separate browser-login rollout.
# The agent starts a login, then gives the returned HTTPS link/code to the person.
opentrain auth login --no-wait --json
# After the person signs in and explicitly approves, resume the SAME handle.
opentrain auth login wait --login-id <loginId> --timeout 120 --json
opentrain auth status --jsonThe person can use a browser on a different computer or phone. The normal OpenTrain approval page displays the matching short code, identity and requested permissions. No local callback, Vercel login, copied API key or password sharing is needed. The agent must not approve on the person's behalf. Browser login does not grant new roles, bypass restricted-admin policy, or approve money movement.
auth login without --no-wait prints the link on stderr and waits up to
--timeout seconds (default 600); stdout carries one final result. --open
explicitly asks to open the link on the CLI machine, which is usually unnecessary
for a remote agent. PENDING is a pending human decision, not authentication.
Resume the same loginId; after DENIED or EXPIRED, a new login needs a new
explicit request. auth login cancel --login-id <loginId> removes that local
pending handle; the server-side approval expires independently.
The login receipt verifies the new browser session, but an environment API key
can still take precedence for later commands. Read the reported credential source
and shadowing warnings before using the account. auth logout revokes and removes
the selected saved browser session; it preserves a separately saved API key and a
new login completed meanwhile. Static-key-only logout retains its existing local
removal behavior. Offline logout reports local removal separately from unconfirmed
server revocation. It never revokes an environment key.
Employer and freelancer Settings also expose Connected agents alongside API keys for inspecting and revoking your own browser-login connections. Revoking one ends that grant's access; it does not revoke unrelated API keys or another user's connection. If the list cannot be loaded, Settings reports it as unavailable and offers a read-only retry rather than claiming there are no connections.
New account only: anonymous agent onboarding
# 1. Create an anonymous agent account with a pre-claim API token
# (job drafting/publishing + read access). Saved to local config.
opentrain auth register --agent-name "My Agent"
# 2. Draft a job from a plain-text description — OpenTrain parses it
opentrain jobs draft create --description-file ./job-description.txt
# 3. Fill any missing fields the draft output asks for
opentrain jobs draft update --job-id <id> --set pricePerHour=25
# 4. Publish when the draft is publish ready
opentrain jobs publish --job-id <id>
# 5. To unlock hiring, messaging, and team invites, hand the account to a human:
opentrain auth claim --email [email protected]
opentrain auth claim-status --waitAPI keys remain supported for CI and existing integrations. Already have a
personal API token (ot_pat_…) from the OpenTrain app?
Create it from Employer Settings -> Developer -> API keys, then copy it once:
opentrain auth login --api-key ot_pat_<publicId>_<secret>
opentrain whoamiCommands
Execution commands accept --pretty for prose instead of the machine envelope.
Registry-declared read-only commands also accept --fields <path,path,…>:
dot paths of data, with element-wise array traversal (e.g.
--fields messages.id,messages.content). Writes, mixed-action commands,
JSONL streams, watch mode and --pretty refuse projection before dispatch.
Malformed paths exit 2. Paths that matched nothing in this result appear in
meta.projection.unmatchedFields with ok: true; empty arrays and nulls
satisfy traversal. meta.projection also names selected paths, omitted
top-level keys (worked out from the request) and retainedPaths — what was
kept without being asked. Continuation, truncation and completeness siblings
(nextCursor, hasMore, truncated, *Truncated, incomplete, complete,
*Complete, partial, valuesExhaustive) are retained, even nested and
even when false — a marker is a boolean, a number, null, a cursor string
(nextCursor / resumeCursor) or a flat map of such flags (the
contracts history truncated block); a marker-named key holding any
other string, an array or a nested object is walked like any subtree, so
only the markers inside it are kept — and the JobOps
sync / syncRecovery blocks are always kept whole
(the next covered call acknowledges them by presenting the token); the API
response is unchanged and rerunning a read without the flag retrieves
everything (when the invocation wrote its payload to a local file —
checklists checkout --dir, a download --out, an export --output —
read the written file instead; fullRetrieval says which, per invocation). Discover fields with a full read first, and retain whole documents
when editing them: text-only projection cannot safely preserve marks or attrs.
Projection adds overhead on small responses; first-use field discovery can cost
more than it saves. See the cold/warm measurements in output-fidelity.json.
Repeated sweeps use conditional reads: --digest adds meta.digest (a
stable content digest of the final data, after --fields), and
--if-none-match <digest> re-runs the same read and, when the fresh data's
digest equals the one you hold, writes data: null with
meta.notModified: true and the continuation meta intact; when something
changed it writes the complete data and its new digest. Only data you proved
you already hold is withheld, and the API call still happens, so a polling
loop is read --fields … --digest once, then read --fields … --if-none-match
<meta.digest> until notModified is false. An invocation that received a
JobOps sync / syncRecovery delivery on any page — the transport records
it, whether or not the block survives in the final data — is never withheld
(its digest is still reported): the token the CLI presents on the next
covered call acknowledges only deliveries you were shown. Sync-covered reads
(every manager … read, the bridged messaging/LMS reads) therefore never
benefit; the conditional form is for the plain reads you sweep repeatedly.
It is refused for an invocation that writes its payload to a local file
(--out / --output / --dir): the receipt's digest does not cover the
file — and when the path arrives inside --input instead, the receipt is
never withheld (the write is recorded as it happens; notModified stays
false). Both are machine-envelope controls (not with --pretty).
When a page DID change, --delta (with --if-none-match) sends only what
moved: every scalar of the page (continuation included) plus the complete
collection records that are new or changed, and meta.delta — the
collection key, the fresh order and the unchanged / changed / added /
removed ids — so you rebuild the exact fresh page from the one you hold
(take changed/added records from data, the rest from your copy, in
meta.delta.order); meta.digest is the fresh page's digest for the next
sweep. It works for pages with one collection of id-bearing records whose
previous shape this CLI remembered (ids and record digests only, in a 0600
state file, for 24 hours); any other case — a page this CLI never
digested, a different collection, no collection — writes the complete
data with no meta.delta, so nothing is ever lost.
Large artifacts go to a file, not through your context. An export with an
output path (lms assignments export --output, lms attempts export
--output, todos quiz coaching export --out) places the COMPLETE payload
atomically (a torn or partial file never appears at the path: the bytes go
to a sibling <path>.partial-<pid>-<hex> renamed into place only once every
byte is written — a short write, a full disk, a quota or a size limit fails
with OUTPUT_WRITE_FAILED rather than placing a truncated file — and an
existing regular file is replaced in place with its exact mode kept; a
symbolic link at the path is refused before any request
(OUTPUT_SYMLINK_REFUSED, naming the link's target so you can pass it
directly — the CLI never writes through a link), as are a directory
(OUTPUT_IS_DIRECTORY), a device/FIFO/socket (OUTPUT_NOT_REGULAR_FILE)
and a file this process cannot write (OUTPUT_NOT_WRITABLE), all exit 2; a
file-size limit (ulimit -f) fails as OUTPUT_WRITE_FAILED rather than
killing the CLI; a signal that kills the CLI mid-write can leave that
partial file behind, safe to delete, with the target untouched; because the
file is replaced by rename, an existing target's hard links stop sharing
its content and you become the file's owner) and prints a
compact receipt — the payload's own keys minus the content, plus
savedBytes, the sha256 of exactly the bytes written and contentType —
never the artifact. lms assignments export --output walks every page from
--cursor on into that one file (json = one array of rows, jsonl = one
row per line, csv = the header once), page by page, and reports
rowCount, pages, startCursor, complete: true (complete from
startCursor), nextCursor: null. Under --output, --limit is the
walk's page size (1–5000), not a row cap — --limit 10 --output f used to
write ten rows and now writes the whole export in pages of ten. A walk the
bounded page cap (200 pages) interrupts writes nothing and fails with
LMS_EXPORT_PAGE_CAP, whose details.resumeCursor resumes into a second
file. Query the file locally (jq, rg, code) and verify it by the
receipt's sha256; the measured workflow (three pages, 27 KB through
stdout versus a 455-byte receipt) is in output-fidelity.json under
large-artifact.
Every other command that writes a path you supply goes through the same
writer with the same refusals, checked before any request (for agreements
preview, before the first page download — the page list comes from the
preview read): open-label
exports download --out (and its server-named default, which is only ever a
plain file name in the current directory), manager setup plan --out,
todos quiz export --out and todos quiz results-export --out, agreements
preview --out <dir> (every page's path, before the first page download) and
agreements requests download --out, videos download save --output,
instructions plan --out and the apply journal beside the plan
(<plan>.journal.json, checked before any request under --confirm-live),
the checklist bundle files checklists init|checkout --dir place, the LMS
preview evidence and audit files staged for publication, and todos quiz
coaching image download --out. agreements requests download,
agreements preview and videos download save keep their no-overwrite
default as a typed refusal — an existing file is OUTPUT_EXISTS (exit 2)
until you pass --force — and place the file exclusively, so a file or link
that appears between the check and the placement is never clobbered either.
checklists init|checkout --dir refuses an existing bundle or base file as
a usage error until you pass --force, and LMS preview evidence publication
never replaces an existing destination (use an empty --out directory). The
other writers (including open-label exports download and todos quiz
coaching image download) replace an existing regular file in place, keeping
its mode, and still never write through a link. Where the envelope reports the file it
now carries savedBytes, sha256 and contentType beside its earlier
fields (additive).
Contract list/get/history responses support the additive dashboard-link
feature negotiation header. CLI 0.27.x sends it automatically; other
HTTP clients can opt in with X-OpenTrain-Features: contract-dashboard-url-v1.
Clients that omit the header receive the legacy response shape.
opentrain --version
opentrain whoami [--pretty]
opentrain auth register [--agent-name <name>] [--org-name <name>] [--base-url <url>] [--force] [--pretty]
opentrain auth claim --email <human-email> [--claim-token <token>] [--pretty]
opentrain auth claim-status [--wait] [--timeout <seconds>] [--claim-token <token>] [--pretty]
opentrain auth login [--no-wait] [--timeout <seconds>] [--open] [--device-label <label>] [--lane EMPLOYER|WORKER] [--scope <scopes>] [--base-url <origin>] [--json]
opentrain auth login status --login-id <loginId> [--json]
opentrain auth login wait --login-id <loginId> [--timeout <seconds>] [--json]
opentrain auth login cancel --login-id <loginId> [--json]
opentrain auth login --api-key <token> [--base-url <url>] [--json]
opentrain auth status [--pretty]
opentrain auth logout
opentrain jobs draft create --description <text> [--title <title>] [--external-id <id>] [--idempotency-key <key>] [--json]
opentrain jobs draft create --description-file <path> [--title <title>] [--external-id <id>] [--idempotency-key <key>] [--json]
opentrain jobs draft create --canonical-file <path> [--external-id <id>] [--idempotency-key <key>] [--json]
opentrain jobs draft create --payload-file <path> [--external-id <id>] [--idempotency-key <key>] [--json]
opentrain jobs draft update --job-id <id> --set <field=value> [--set <field=value> ...] [--json]
opentrain jobs draft update --job-id <id> --patch-file <path> | --patch-json <json> [--json]
opentrain jobs list [--status <status>] [--limit <n>] [--cursor <cursor>] [--json]
opentrain jobs search [--q <text>] [--category <slug>] [--language <lang>] [--country <iso>] [--pay-type PAY_PER_HOUR|FIXED_PRICE|PAY_PER_LABEL] [--limit <n>] [--cursor <cursor>] [--json]
opentrain jobs publish --job-id <id> [--json]
opentrain jobs invite --job-id <id> --freelancer-id <id> [--json]
opentrain jobs close --job-id <id> [--json]
opentrain jobs update-published --job-id <id> --set <field=value> [--set <field=value> ...] [--json]
opentrain jobs update-published --job-id <id> --patch-file <path> | --patch-json <json> [--json]
opentrain jobs interview set --job-id <id> (--required | --not-required | --default) [--modality voice|text] [--json]
opentrain contracts list [--job-id <id>] [--status active|ended] [--participation-state ACTIVE|OFFBOARDING|OFFBOARDED_PAYABLE|CLOSED|REACTIVATED] [--json]
opentrain contracts get --contract-id <id> [--json]
opentrain contracts history (--contract-id <id> | --proposal-id <id>) [--view current|timeline] [--json]
opentrain contracts end --contract-id <id> [--json]
opentrain milestones create --contract-id <id> --description <text> [--name <name>] [--amount <usd>] [--volume <n>] [--due-date <iso>] [--json]
opentrain milestones fund --milestone-id <id> [--json]
opentrain milestones approve --milestone-id <id> [--json]
opentrain approvals get --approval-id <id> [--json]
opentrain proposals list --job-id <id> [--status <status>] [--sort recent|screening-score] [--limit <n>] [--cursor <cursor>] [--json]
opentrain proposals get --proposal-id <id> [--interview] [--json]
opentrain proposals interview-audio --proposal-id <id> [--output <path>] [--url-only] [--force] [--json]
opentrain proposals hire --proposal-id <id> (--amount <usd> | --hours <n>) [--rate <usd/hour>] [--milestone-name <name>] [--milestone-description <text>] [--due-date <iso>] [--confirm-not-fit-override] [--json]
opentrain contracts propose-rate --contract-id <id> --rate <usd/hour> [--reason <text>] [--idempotency-key <key>] [--json]
opentrain freelancers get --id <user-id-or-slug> [--json]
opentrain talent search [--q <text>] [--skill <s>] [--language <l>] [--country <ISO>] [--badge VERIFIED_TRAINER] [--talent-type Individual|Agency] [--availability <text>] [--min-reputation <n>] [--tier PROVEN|ESTABLISHED|PROVISIONAL] [--available-now] [--sort relevance|reputation|rate_asc|rate_desc|recent] [--limit <n>] [--offset <n>] [--json]
opentrain talent reputation --slug <profile-slug-or-user-id> [--json]
opentrain messages list [--filter all|job|proposal] [--unread-only] [--limit <n>] [--cursor <cursor>] [--json]
opentrain messages unread [--filter all|job|proposal] [--limit <n>] [--cursor <cursor>] [--json]
opentrain messages read --conversation-id <id> [--limit <n>] [--direction older|newer] [--cursor <cursor>] [--json]
opentrain messages send --conversation-id <id> --content <text> [--json]
opentrain messages send --conversation-id <id> --content-file <path> [--json]
opentrain messages edit --message-id <id> --expected-revision <n> --idempotency-key <key> (--content <text> | --content-file <path|->) [--json]
opentrain messages delete --message-id <id> --expected-revision <n> --idempotency-key <key> [--json]
opentrain messages start-proposal-thread --proposal-id <id> [--json]
opentrain updates poll [--cursor <cursor>] [--limit <n>] [--json]
opentrain credits show [--json]
opentrain credits ledger [--cursor <cursor>] [--limit <n>] [--json]
opentrain credits top-up --amount <usd> [--json]
opentrain credits top-up-status --top-up-id <id> [--json]
opentrain webhooks create --url <url> --events <type,...> [--json]
opentrain webhooks list [--json]
opentrain webhooks get <webhook-id> [--json]
opentrain webhooks delete <webhook-id> [--json]
opentrain tokens list [--json]
opentrain tokens revoke --token-id <id> [--json]
opentrain team show [--json]
opentrain team invite --email <address> [--json]
opentrain payments pending [--json]
opentrain capabilities [--json]
opentrain jobs draft create --description <text> [--title <title>] [--external-id <id>] [--idempotency-key <key>] [--pretty]
opentrain jobs draft create --description-file <path> [--title <title>] [--external-id <id>] [--idempotency-key <key>] [--pretty]
opentrain jobs draft create --canonical-file <path> [--external-id <id>] [--idempotency-key <key>] [--pretty]
opentrain jobs draft create --payload-file <path> [--external-id <id>] [--idempotency-key <key>] [--pretty]
opentrain jobs draft update --job-id <id> --set <field=value> [--set <field=value> ...] [--pretty]
opentrain jobs draft update --job-id <id> --patch-file <path> | --patch-json <json> [--pretty]
opentrain jobs list [--status <status>] [--limit <n>] [--cursor <cursor>] [--pretty]
opentrain jobs search [--q <text>] [--category <slug>] [--language <lang>] [--country <iso>] [--pay-type PAY_PER_HOUR|FIXED_PRICE|PAY_PER_LABEL] [--limit <n>] [--cursor <cursor>] [--pretty]
opentrain jobs publish --job-id <id> [--pretty]
opentrain jobs invite --job-id <id> --freelancer-id <id> [--pretty]
opentrain jobs invitations list --job-id <id> [--freelancer-id <id>] [--state INVITED|DECLINED|APPLIED|HIRED] [--limit <n>] [--cursor <cursor>] [--all] [--pretty]
opentrain jobs invitations get --job-id <id> --freelancer-id <id> [--pretty]
opentrain jobs close --job-id <id> [--pretty]
opentrain jobs draft discard --job-id <id> --confirm [--pretty]
opentrain jobs draft clients [--pretty] (admin accounts: valid msClientId values)
opentrain jobs update-published --job-id <id> --set <field=value> [--set <field=value> ...] [--pretty]
opentrain jobs update-published --job-id <id> --patch-file <path> | --patch-json <json> [--pretty]
opentrain jobs interview set --job-id <id> (--required | --not-required | --default) [--modality voice|text] [--pretty]
opentrain jobs interview help (what each setting means; --default = required unless an Open Label work assessment replaces it)
opentrain contracts list [--job-id <id>] [--status active|ended] [--limit <n>] [--cursor <cursor>] [--all] [--pretty]
opentrain contracts get --contract-id <id> [--pretty]
opentrain contracts history (--contract-id <id> | --proposal-id <id>) [--view current|timeline] [--pretty]
opentrain contracts end --contract-id <id> [--pretty]
opentrain milestones create --contract-id <id> --description <text> [--name <name>] [--amount <usd>] [--volume <n>] [--due-date <iso>] [--pretty]
opentrain milestones fund --milestone-id <id> [--pretty]
opentrain milestones approve --milestone-id <id> [--pretty]
opentrain admin-balance-milestones create --contract-id <id> --idempotency-key <key> --description <text> --amount <usd> [--volume <hours-or-labels>] [--name <name>] [--due-date <iso>] [--pretty]
opentrain admin-balance-milestones fund --contract-id <id> --milestone-id <id> --idempotency-key <key> [--pretty]
opentrain approvals get --approval-id <id> [--pretty]
opentrain proposals list --job-id <id> [--status <status>] [--sort recent|screening-score] [--limit <n>] [--cursor <cursor>] [--pretty]
opentrain proposals get --proposal-id <id> [--interview] [--pretty]
opentrain proposals interview-audio --proposal-id <id> [--output <path>] [--url-only] [--force] [--pretty]
opentrain proposals hire --proposal-id <id> (--amount <usd> | --hours <n>) [--rate <usd/hour>] [--milestone-name <name>] [--milestone-description <text>] [--due-date <iso>] [--confirm-not-fit-override] [--pretty]
opentrain contracts propose-rate --contract-id <id> --rate <usd/hour> [--reason <text>] [--idempotency-key <key>] [--pretty]
opentrain freelancers get --id <user-id-or-slug> [--pretty]
opentrain talent search [--q <text>] [--skill <s>] [--language <l>] [--country <ISO>] [--badge VERIFIED_TRAINER] [--talent-type Individual|Agency] [--availability <text>] [--min-reputation <n>] [--tier PROVEN|ESTABLISHED|PROVISIONAL] [--available-now] [--sort relevance|reputation|rate_asc|rate_desc|recent] [--limit <n>] [--offset <n>] [--pretty]
opentrain talent reputation --slug <profile-slug-or-user-id> [--pretty]
opentrain messages list [--filter all|job|proposal] [--unread-only] [--limit <n>] [--cursor <cursor>] [--pretty]
opentrain messages unread [--filter all|job|proposal] [--limit <n>] [--cursor <cursor>] [--pretty]
opentrain messages read --conversation-id <id> [--limit <n>] [--direction older|newer] [--cursor <cursor>] [--pretty]
opentrain messages send --conversation-id <id> --content <text> [--pretty]
opentrain messages send --conversation-id <id> --content-file <path> [--pretty]
opentrain messages edit --message-id <id> --expected-revision <n> --idempotency-key <key> (--content <text> | --content-file <path|->) [--pretty]
opentrain messages delete --message-id <id> --expected-revision <n> --idempotency-key <key> [--pretty]
opentrain messages start-proposal-thread --proposal-id <id> [--pretty]
opentrain updates poll [--cursor <cursor>] [--limit <n>] [--pretty]
opentrain credits show [--pretty]
opentrain credits ledger [--cursor <cursor>] [--limit <n>] [--pretty]
opentrain credits top-up --amount <usd> [--pretty]
opentrain credits top-up-status --top-up-id <id> [--pretty]
opentrain webhooks create --url <url> --events <type,...> [--pretty]
opentrain webhooks list [--pretty]
opentrain webhooks get <webhook-id> [--pretty]
opentrain webhooks delete <webhook-id> [--pretty]
opentrain tokens list [--pretty]
opentrain tokens revoke --token-id <id> [--pretty]
opentrain team show [--pretty]
opentrain team invite --email <address> [--pretty]
opentrain payments pending [--pretty]
opentrain capabilities [--pretty]
opentrain instructions help
opentrain todos help
opentrain lms help
opentrain manager helpEvery non-stream command returns one versioned JSON success or failure
envelope by default. Pass --pretty for the human-readable view. Existing
--json calls remain valid, but the flag is a deprecated no-op during this
transition release.
Shared job operations (manager)
opentrain manager help (CLI 0.11.0+) is the context-first control surface for
managing an existing employer job alongside other people and agents. One
manager context get --view compact call automatically carries the top-10
fresh priority attention signals with their derived claim state, signal counts,
per-rule evaluator health, the effective typed policies, and the
Agent Guide digest — there is no second bootstrap command.
Automatic by design — the human never needs to know to ask for this setup.
The first job-scoped manager call starts or resumes the durable per-agent
sync session, and its bootstrap envelope directs exactly one read of the
composite Agent Guide (sync.nextOperations ends with
job_operations.agent_guide.get — manager agent-guide get: the canonical
typed projection plus the human-published Playbook); afterwards, whenever the
Playbook head moves past what the agent could have held, the covered
response's recommendation.agent_guide_changed names the exact manager
playbook delta read (added/changed sections, removedSectionIds tombstones)
so it refreshes only what changed, and the agent acknowledges only a revision
that was actually delivered to it. Agents define
this job's own work types and workflow in its Work Catalog (manager work …)
instead of fixed grading/retraining enums, and propose missing or outdated
guidance as Playbook proposals (manager playbook proposals submit;
RECOMMENDED, never enforced) — only the human job manager publishes
(manager playbook publish). The composite Agent Guide, Playbook reads and
deltas, proposals, and publication need CLI 0.26.0+.
Every manager mutation (CLI 0.16.0+) has command-specific discovery: leaf
--help (e.g. opentrain manager actions create --help) prints that
operation's exact required/optional body fields with types, enums, and bounds,
the dedicated-flag partition, evidence guidance, and a copy-paste example
accepted by the real server validator; --print-schema on the same commands
prints the machine-readable opentrain-cli-schema/1 envelope with zero
authentication and zero network. Schema-discovery-by-production-400 is over.
CLI 0.11.0 automatically keeps every covered job-scoped call synchronized. It
stores a server-minted session credential and the latest per-job sync tokens in
a private, per-agent-instance state file, then threads them without prompting.
Every covered JSON response includes sync and syncCoverage; inspect
sync.conflicts, sync.recommendations, and sync.nextOperations before the
next action. Raw credentials and signed tokens never appear in CLI output.
Deleting local sync state is safe—it only causes an evidence-backed bootstrap.
syncCoverage.coverage is exhaustive: native and bridged calls refresh
shared context, while none explains why that operation does not. A durable
agent instance is taken only from PASEO_AGENT_ID or
OPENTRAIN_AGENT_INSTANCE; without one the CLI safely stays ephemeral instead
of accidentally sharing state through a working directory. Ephemeral covered
calls additionally receive the advisory-only
recommendation.establish_durable_agent_session envelope entry naming the
exact fix (export a unique per-agent-instance value BEFORE running commands);
never reuse one instance value across concurrent agents — that would share a
cursor.
manager sync pollwaits up to 25 seconds for a fresh shared-context update;--watchemits one bounded JSON response per poll.manager intents declare|list|resolveis the write-ahead coordination lane. Declare before spontaneous work so another manager sees ownership and a duplicate agent receives the live winner instead of racing.manager proposals submit|list|review|withdrawrecords a non-enforced alternative when the requested workflow conflicts with current team policy, claims, or intent. Reviewing a proposal never executes its suggested change.manager overlays get|setkeeps guidance at its correct scope: TEAM is owner-controlled shared guidance, PERSONAL is one manager's durable memory, and SESSION is one agent instance's scratch memory. Overlays never change canonical gates or policies.manager messages sendis the synchronized live-job send path. It requires a stable--idempotency-keyand the complete claimed-action guard, including the exact latest authorized root/thread message id (or literalnoneonly when the conversation was observed empty). The server resolves the canonical Job Message or Proposal Message job, commits the message and context advance atomically, and refuses stale or cross-job sends. It requires bothmessages:manageandmessages:write; ordinarymessages sendremains the backward-compatible unsynchronized path.manager signals list|get|acknowledge|snooze|dismiss|claim— deterministic attention signals, highest priority first.claimis the ONLY path that turns a signal into owned work (triage never claims) and requires the exact--expected-signal-revision; eligibility is ACTIVE + FRESH + effective OPEN/ACKNOWLEDGED, non-shadow.manager policies effective|list|record|verify-enforcement— typed, attributed, superseding durable policies (never chat or free text).manager guide get [--format json|markdown]— the canonical typed projection of the Agent Guide alone (without the Playbook; guideVersion + SHA-256 checksum), kept for published clients — new work readsmanager agent-guide get. The same Markdown is the MCP resourceopentrain://jobs/{jobId}/operations/guide.manager setup status|plan|apply(CLI 0.15.0+) — the guided operational setup flow. Shared setup is product-driven: while a job's operating brief, typed workflow, or an applicable required operating choice is missing, every covered response's sync envelope carries the deterministicrecommendation.job_setup_incompleteentry pointing here. The DRIVEN multi-agent flow runs in order:status(machine packet +inProgressview) →manager intents declare … --intent-key setup.guided(a DUPLICATE names the live winner but does not transfer ownership) →plan … --adopt-intent <yourIntentId>→ human approval →apply … --confirm-live→ verify, resolve your intent, hand off.plancomposes the exact canonical writes ZERO-WRITE with a deterministicplanReceipt, never guesses an unanswered required choice, and fails closed on a truncatedinProgressview, foreign activesetup.*intents/proposals, or INDETERMINATE evidence (follow the packet'sevidenceRecoveryread).applystrictly re-validates the whole plan against the closed guided-setup grammar before ANY request leaves the process — exactly the canonical brief publish and guided typed-policy records for this job, exact paths, content-bound idempotency keys, no unknown fields or duplicates; theplanReceiptis integrity/approval evidence, never authority. It also fails closed without--confirm-live, on shared-context drift, on INDETERMINATE evidence, and on foreignsetup.*coordination. Bothplanandapplyprove ownership by exact durable-session identity: the intent'ssessionIdmust equal the current response'ssync.session.sessionIdwith a matching principal. A winner ID visible in a DUPLICATE result is never adoptable, and a copied plan cannot be applied from another session. The command then executes and prints the verified shared state. Once setup reports COMPLETE or DEFERRED, stop prompting — completion is shared across the employer team. An approved postponement is an ordinaryOPERATING_RULE.setup_deferral_*typed policy.manager work definitions|executions|migrations …(CLI 0.23.0+, DEV-3191) — the dynamic per-job Work Catalog. Work is PER-JOB catalog data, never a platform enum: every covered response'ssync.workCatalogteaches the catalog head/status, the winner-only setup lease, pending human approvals, and this job's work-coordination mode. Definition-bound mutations present the FULL binding (versionNumber + contentHash + coordinationSignature); every execution mints ONE coordinating action that is the single claim authority. An INFERRED proposal never activates without the typedWORK_DEFINITION_APPROVAL.<definitionKey>policy recorded by a non-agent human job manager, after every possible-duplicate candidate needing a decision is resolved (WORK_DUPLICATE_RESOLUTION.<first>.<second>; advisory same-shape evidence never blocks).Work-coordination mode (
sync.workCatalog.coordination): the singleton typed policyWORK_COORDINATION_MODE— recorded only by a non-agent human job manager — declares how strictly this job requires declared work:OPEN(default when no row exists; the catalog is advisory),COORDINATED(every guarded mutation such asmanager messages sendmust present a claimed action bound to an OPEN work execution, defined or provisional),MANAGED(that execution must be DEFINED work under an ACTIVE definition). Anullmode is fail-closed evidence (no current head, a branched lineage, or an unreadable head) — never OPEN. A refused guarded mutation is a 409 whosedetailscarryreason: JOB_OPS_WORK_COORDINATION_REQUIRED,requirement(WORK_EXECUTION_REQUIRED|DEFINED_WORK_REQUIRED|MODE_UNREADABLE), andmode; the JSON error envelope passes them through unchanged.External attestations (
manager observations recordwith authorityOBSERVEDand a top-levelexternalAttestation): the catalog cannot see browser edits, chat/ticket/spreadsheet tools, or out-of-band mutations, so that state is UNKNOWN unless a CURRENT attestation covers it.ATTESTcarries a bounded per-jobsource.systemslug, optional locator/subject, a claim, a non-futureattestedAt, a REQUIREDexpiresAt(≤ 30 days), and an optionalsupersedesObservationId;INVALIDATEnames the attestation a webhook, UI, agent, or human retired. Attribution is canonical: pass the top-levelactorAgentLabel(stored ascreatedByAgentText); a nested label is refused. The server proves the same-context lineage before writing (readable non-future target, same source + subject, one live ender per target, ONE current head per source + subject — a second root is refused and told which head to supersede).manager guide getrendersexternalCoverage: per-entry status, attribution, bounded evidence, and the ending receipt (who ended it, why, when, evidence).
When a message suggests repeated confusion, that is an agent inference—not a
deterministic OpenTrain alert. Record it as INFERRED, show a zero-write LMS
retraining outline, and obtain explicit human approval before creating or
assigning retraining. Recommendations and proposals are never write authority.
These commands require an OpenTrain server exposing the
job_operations.signals.* / job_operations.policies.* /
job_operations.guide.get operations (opentrain capabilities lists them);
the base manager family needs CLI 0.8.0+, the attention/policy/guide commands
need CLI 0.10.0+, automatic sync/intents/proposals/overlays need CLI
0.11.0+, guided setup (manager setup) needs CLI 0.15.0+, the Work
Catalog, coordination modes, and external attestations need CLI 0.23.0+, and
the composite Agent Guide + Playbook family (manager agent-guide get,
manager playbook …) needs CLI 0.26.0+.
Shared folder Instructions (0.27.0+)
Use the same canonical editor pages across every eligible child job. Discover and review before activation; ordinary reads never hide job guidance:
opentrain instructions folder inspect --folder <folderId> --include-content
opentrain instructions folder preview --folder <folderId> --request request.json
opentrain instructions folder confirm --folder <folderId> --request request.json --plan-digest <reviewedDigest> --confirm
opentrain instructions folder status --folder <folderId> --operation <operationId>
# Wait until status.quiesceReadyAt, then apply the same operation.
opentrain instructions folder apply --folder <folderId> --operation <operationId> --confirm
opentrain instructions folder recovery --folder <folderId> --operation <operationId>Preview lists every affected job, copied/hidden page, destination order and
expanded audience. Activation preserves originals/checkpoints/media and commits
the complete destination atomically. Require VERIFIED_READBACK; after an
uncertain response inspect the same operation, never create another key.
Cancel/resume and guarded reversal are described in instructions folder help.
After activation, use exactly one selector, --folder <folderId> or
--job <jobId>, for page/tree, import/assets, links, history, schema, bundles
and verify. Existing job clients remain compatible. Folder authoring refuses an
empty target until reviewed activation and refuses incompatible published/unknown
delivery without changing release/read/ack flags. Last-page archival does not
restore hidden job pages. See the folder workflow.
Canonical job instructions
opentrain instructions reads and edits the same pages shown in the actual
job Instructions tab. A job ID is mandatory; there are no standalone
workspaces, manuals, bindings, or publication copies.
opentrain instructions inspect --job <job-id>
opentrain instructions page get --job <job-id> --page <page-id>
opentrain instructions page link --job <job-id> --page <page-id> \
--anchor <stable-node-id>
opentrain instructions page preview --job <job-id> \
--content-file ./handbook.tiptap.json
opentrain instructions page create --job <job-id> --title "Reviewer handbook" \
--key reviewer-handbook-v1 --content-file ./handbook.tiptap.json \
--confirm-live
opentrain instructions import docx --job <job-id> --file ./handbook.docx \
--title "Reviewer handbook" --key reviewer-handbook-docx-v1 --confirm-live
opentrain instructions page patch --job <job-id> --page <page-id> \
--expected-checksum <sha256> --ops-file ./patch.json --confirm-live
opentrain instructions verify --job <job-id>
opentrain instructions visual-check --job <job-id>
# Controlled multi-page work: checkout → edit files → diff/validate → immutable
# plan → review plan JSON → one atomic canonical-draft tree swap.
opentrain instructions checkout --job <job-id> --dir ./job-instructions
opentrain instructions diff --dir ./job-instructions
opentrain instructions validate --dir ./job-instructions
opentrain instructions plan --dir ./job-instructions --out ./job-instructions.plan.json
opentrain instructions apply --plan ./job-instructions.plan.json --confirm-live
opentrain instructions verify --job <job-id> --against-bundle ./job-instructionsEvery page reports its contentSchemaVersion. Schema v1 retains the original
job-document node set; schema v2 adds details, callouts, YouTube, audio, and
attachment blocks. Upgrade an existing page explicitly, then re-check it out
before authoring v2 content:
# No-write status and impact plan
opentrain instructions page upgrade-schema --job <job-id> --page <page-id>
# One-way v1 → v2 upgrade. --wait follows the quiescence window to completion.
opentrain instructions page upgrade-schema --job <job-id> --page <page-id> \
--target 2 --confirm-live --wait
# Convert a supported YouTube URL to the canonical safe TipTap node offline.
opentrain instructions youtube-node --url "https://youtu.be/jNQXAC9IVRw" \
--title "Project walkthrough"Fresh checkouts use bundle format opentrain-job-instructions-bundle/2 and
record the schema version of every page. Older version-blind bundles fail
closed; run a fresh checkout instead of hand-editing their manifest. YouTube
URLs are normalized to the privacy-enhanced player and arbitrary iframe HTML,
playlists, tracking parameters, and non-YouTube hosts are rejected.
Job instruction writes change the canonical employer draft. Create, replace,
and DOCX import preview without writing until you add --confirm-live; every
other draft mutation requires that flag. Before a shared-draft write,
the CLI obtains a short-lived signed preflight token bound to the exact job,
actor, operation, content, and canonical state. The server revalidates it, then
the CLI reads the canonical page back and verifies its checksum or removal.
Every successful operation returns the exact employer URL where the result is
visible.
Checkout directories are non-authoritative working copies. plan fails on
remote drift and records the exact archive set and normalized content reviewed
by the operator. apply stages deterministic TipTap documents and swaps the
entire page tree in one database transaction, so workers never see a partially
assembled multi-page manual. Interrupted retries converge using the same
idempotency key; regenerate the plan instead of editing plan JSON.
Pages support multiple roots plus one subpage level, matching the job editor: for example,
Start Here, Annotators, QC, and Language-Specific Pages can each be level-one roots,
with direct children beneath them. A child cannot parent another page. Do not put every page
under Start Here; use a separate root when a section is independent.
For page creation, --parent <pageId> makes a new page a direct child of an existing root;
omit --parent to create a root. For an existing page, page move --parent <rootPageId>
reparents it under that root, while page move --root promotes it to a root and
page move --direction up|down changes sibling order. The one-level guard refuses a parent
that is already a child, so creating a second nested level requires a root/child rewrite. A page
that already has children cannot be moved beneath another page; move its children first.
# Independent level-one roots
opentrain instructions page create --job <jobId> --title "Start Here" --key <key> --confirm-live
opentrain instructions page create --job <jobId> --title "Annotators" --key <key> --confirm-live
# One direct child beneath an existing root
opentrain instructions page create --job <jobId> --title "Annotation Rules" \
--parent <annotators-page-id> --key <key> --confirm-live
# Reorganize an existing page
opentrain instructions page move --job <jobId> --page <pageId> \
--parent <annotators-page-id> --confirm-live
opentrain instructions page move --job <jobId> --page <pageId> --root --confirm-liveFolder ACTIVATE and APPEND share the same reference: each newPages[].parentRef is either
null for another new root or the ref of a root in the same newPages request. It is not an
existing folder page ID. Use page create --folder <folderId> / page move --folder <folderId>
for existing pages. A page with children cannot be moved beneath another page until its children
move first. Page content is worker-visible, so keep editor-only setup or internal notes out of
published pages; in an internal authoring brief, a clearly labelled EDITOR-ONLY — PAGE SETUP
codeBlock or callout can distinguish setup from ordinary guide prose and real cross-links below.
DOCX
import converts headings, lists, tables, formatting, links, and embedded
images deterministically. Preview returns the embedded-image manifest and
stable final URL plan without uploading; confirmed import uploads those exact
bytes and inserts the canonical URLs. Direct image uploads accept JPEG, PNG,
WebP, GIF, and AVIF up to 4 MB. Whole-document replacement additionally
requires the last-read checksum and --confirm-replace; normal rich edits use
checksum-guarded node patches. Run opentrain instructions page patch help for
the six stable-node operations and examples.
visual-check returns the authenticated employer URL, exact sidebar order,
page checksums, expected images, viewports, and screenshot assertions. It does
not pretend an employer token proves the worker reader: release verification of
that surface requires a real test worker/contract instruction session.
Search runs the employer editor's ranked full-text search (typo-tolerant
fallback) over the job's effective draft scopes — its own set plus the folder
set it inherits, exactly what inspect and page get read; never a worker's
released view. Results are page groups with up to five block hits each (stable
block ids, anchor ids for deep links, the nearest heading, inert snippets with
code-point highlight ranges). truncated: true means the ranked window filled
— narrow the query rather than page on; hasMore/nextCursor page the ranked
page groups (the cursor is bound to the job, the query and the ranked window: a stale one is
INVALID_CURSOR). --fields keeps the
three markers.
opentrain instructions search --job <job-id> --q "lockout tagout" --limit 5
opentrain instructions search --job <job-id> --q "lockout tagout" --cursor <next-cursor>Read Required compliance is the employer matrix the dashboard shows: one row
per participating contract with the worker's state against the job's published
releases (NO_REVIEW_REQUIRED / SYNCING / ACTION_REQUIRED / MARKED_READ
/ UNDERSTOOD), the outstanding count and the last read / understood
timestamps. Workers are referenced by workerUserId with a masked display
name; worker acknowledgments themselves never cross the public API. The read
is gated by the Read Required rollout for the actor (outside it: 404, like
the release commands) and pages by hasMore/nextCursor.
opentrain instructions compliance --job <job-id> --limit 50
opentrain instructions compliance --job <job-id> --cursor <next-cursor> --prettyThe revision read is the cheap change poll: the job set's revision counter and
the inherited folder set's, folded into one comparable, job-bound
revisionToken (also the strong HTTP ETag, so raw callers get 304 with
If-None-Match until something changed; the body carries no clock, so the
CLI's --digest / --if-none-match are stable across unchanged polls too).
A different token means a committed structure or content change — it never
says what; read inspect, page get, history or release list. There is
no durable event log or webhook.
opentrain instructions revision --job <job-id>
opentrain instructions revision --job <job-id> --if-none-match <digest> # data withheld while unchangedDraft history exposes the employer-only page checkpoints captured automatically on every canonical draft write (and by sweep recovery), plus explicit manual checkpoints and a destructive restore:
opentrain instructions history --job <job-id> --page <page-id>
opentrain instructions history show --job <job-id> --page <page-id> \
--checkpoint <checkpoint-id>
opentrain instructions history compare --job <job-id> --page <page-id> \
--checkpoint <checkpoint-id> --to current
opentrain instructions history checkpoint --job <job-id> --page <page-id> \
--key <idempotency-key>
opentrain instructions history restore --job <job-id> --page <page-id> \
--checkpoint <checkpoint-id> --expected-checksum <sha256> \
--key <fresh-idempotency-key> --confirm-restore --confirm-liveListing uses opaque page-bound cursors (--cursor, from nextCursor).
Compare diffs the checkpoint against the current draft (or another
checkpoint): ADDED blocks exist only in the current draft and would be
removed by a restore; REMOVED blocks would be brought back. Restore is
destructive: it replaces the live page content, so it refuses to run without
--expected-checksum from a fresh compare plus both --confirm-restore and
--confirm-live, always records a durable undo baseline checkpoint first, and
returns 409 if the live page changed since the checksum was read — compare
again, take the fresh checksum, and retry with a NEW --key. After success the
CLI verifies storage + checksum readback only — never collaboration sync or
browser rendering; confirm visually in the job Instructions tab.
For accounts in the immutable-release rollout, workers read snapshots captured from the canonical job draft. Prepare, preview, and publish are explicit sequential operations; publishing requires a delivery policy on every surface:
opentrain instructions release prepare --job <job-id>
opentrain instructions release preview --job <job-id> --candidate <candidate-id>
opentrain instructions release publish --job <job-id> --candidate <candidate-id> \
--operation-key <key> --delivery silent|notify|read-required --confirm
opentrain instructions release status --job <job-id>Candidates expire after 30 minutes. silent changes the effective release
without a worker notification, notify sends a lightweight notification, and
read-required creates an acknowledgement obligation plus reminders. The
release records the server recommendation and any employer override.
Actors outside the rollout receive 404 from release commands. Whether a
confirmed write reaches workers at once or only on publish is stated per read by
opentrain instructions capabilities (mutations.workerVisibility).
Training/LMS
opentrain lms works on the same canonical course draft as the dashboard. A
dashboard-owned and CLI-owned copy never exist. Capability discovery tells an
agent the active shared TipTap profile, registry version, per-block
author/edit/render support, private-asset semantics, active schema, asset
limits, assessment settings, job linking support, and Training controls before
it generates a bundle. Lesson contentDoc values are canonical TipTap JSON;
agents must not generate HTML or a CLI-only content model:
opentrain lms capabilities
opentrain lms courses create --title "Reviewer onboarding" \
--origin-job <job-id> --key reviewer-onboarding-v1
opentrain lms courses recover-create --key reviewer-onboarding-v1LMS reads also return canonical human-navigation links[]. The --pretty
view prints them under Canonical links:; the default JSON envelope preserves
each link's kind, audience, URL, and scoped identifiers. Send EMPLOYER links to employers
(job Training, job-scoped course/preview/version/module/lesson/review/release
impact) and only returned LEARNER links to assigned workers (their Manage Job
→ Project To-dos shell). Do not reconstruct routes from IDs or replace the
server-returned origin. URLs contain no credentials and are reauthorized when
opened; locked future learner lessons are not returned.
The safe hybrid authoring loop is checkout → edit → diff → reconcile when the dashboard changed → validate → push. Pull only fast-forwards a locally clean bundle; reconcile merges non-overlapping local and dashboard edits and refuses true conflicts. Push checks the remote draft revision before any asset upload and again before the draft mutation. The dashboard can reopen and edit every current-profile block emitted by the CLI, and checkout/push preserves those blocks, marks, attributes, ordering, and asset IDs without conversion through the compact Native Forms editor.
Courses can persist an advisory authoringIntent in course.yaml and the
canonical draft tree. It controls authoring validation and worker-equivalent
Preview defaults without becoming a second delivery engine. Legacy courses
default to STANDARD + SYSTEM; INDIVIDUAL_MASTERY requires SYSTEM.
Checkout, diff, reconcile, push, publish, and immutable versions preserve the
declaration. Assignment and retraining records remain authoritative for what a
worker actually receives.
opentrain lms checkout --course <course-id> --dir ./course
opentrain lms diff --dir ./course
opentrain lms pull --dir ./course
opentrain lms reconcile --dir ./course
opentrain lms validate --dir ./course --remote --key validate-course-v1
opentrain lms push --dir ./course --key push-course-v1Private assets use prepare → direct-to-storage upload → verified finalize.
Videos above one 6 MiB chunk use resumable TUS automatically; the current
effective direct-upload video ceiling is reported by lms capabilities (256
MiB at this release). Upload grants are transient secrets and are never printed.
opentrain lms assets upload --course <course-id> --kind VIDEO \
--file ./walkthrough.mp4 --key walkthrough-video-v1
opentrain lms assets upload --course <course-id> --kind AUDIO \
--file ./listen-first.mp3 --key listen-first-audio-v1
opentrain lms assets list --course <course-id>
opentrain lms assets get --course <course-id> --asset <asset-id>Quiz questions can carry media too (image, audio, or video on the prompt, a
title/instructions block, or a section description). Upload the clip against
the assessment with the same prepare → upload → finalize contract, then
reference the returned asset id from the question's promptDoc as an
image, audio, or video node. The kind is derived from the file
extension (.png .jpg .gif .webp / .mp3 .m4a .ogg .wav / .mp4 .webm
.mov); ceilings are 5 MiB images, 32 MiB audio, 256 MiB video. The server
re-verifies the stored bytes (container signature + SHA-256) before the asset
becomes READY, and publishing pins referenced assets to the version.
opentrain lms assessments assets upload --assessment <form-id> \
--file ./listen-and-answer.mp3 --key q3-audio-v1
opentrain lms assessments assets list --assessment <form-id>Publishing, job linking, and assigning learners are separate confirmed
actions. None silently performs another, with one convenience: linking a
TRAINING course also turns on the job's Training and Project To-dos settings
(Project To-dos only when the token also has project_todos:write; the
response's jobSettingsEnabled says which ones it turned on), so
lms training set is only needed to turn Training off or on by itself.
lms courses create --origin-job <job-id> only records which job a course is
for: the course is not on that job until it is linked. lms training status
reports training.readiness: ready, or warnings (ORIGIN_COURSES_NOT_LINKED,
TRAINING_SETTINGS_OFF) with the exact commands to run. Check it before
telling anyone the training is live.
Omitting --stage when the course is already linked keeps the link as it is,
so re-running a link never demotes a SCREENING course:
opentrain lms publish --dir ./course --confirm-publish --key publish-course-v1
opentrain lms training link --job <job-id> --course <course-id> \
--confirm-link --key link-course-v1
# Application-stage screening: every NEW proposal on the job is enrolled
# automatically (published course required; at most one per job).
opentrain lms training link --job <job-id> --course <screening-course-id> \
--stage SCREENING --max-attempts 2 --confirm-link --key screen-course-v1
opentrain lms training set --job <job-id> --enabled true \
--confirm-training --key enable-training-v1
opentrain lms assign --job <job-id> --course <course-id> --version <version-id> \
--audience SELECTED --selected-contracts <contract-id> --policy SYSTEM \
--confirm-assign --key assign-course-v1opentrain lms assign help, lms status help, and lms versions help (or
--help) print the focused leaf usage before any validation or network call.
Assignment status is paged and truthful: every page carries hasMore plus
nextCursor, --pretty says PARTIAL RESULT with the exact next-page
command (never treat a partial page as the complete learner population), and
--all collects every page — effective page size sent on each request,
de-duplicated by assignment id, repeated-cursor safe, with collection
metadata — and fails the command on any page error:
opentrain lms status --job <job-id> # first page (50)
opentrain lms status --job <job-id> --cursor <nextCursor> # next page
opentrain lms status --job <job-id> --limit 200 # custom page size
opentrain lms status --job <job-id> --all # complete retrieval
opentrain lms status --assignment <assignment-id> --include-runs # one record
opentrain lms versions get --course <course-id> --course-version <version-id>To see which quiz questions people find hard, read the per-question results
for a job. Counts use each person's first submitted attempt at each quiz
version (employer review remediation is excluded), grouped by course, lesson
and quiz version: right / wrong / partly right / skipped / awaiting-grade
counts, choice counts with the correct choice marked, and who missed each
question with their answer. Names are always masked as "First L.". Hired
workers are the default audience; --audience APPLICANT reads screening
quizzes. --pretty prints one line per quiz with its hardest question:
opentrain lms question-results --job <job-id> --pretty
opentrain lms question-results --job <job-id> --audience APPLICANT --course <course-id>Course and version reads carry a privacy-safe attribution block (creator,
last material updater, version author, publisher) with stable actor ids and an
expli
