@opentrain-ai/cli
v0.23.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,
Downloads
1,460
Maintainers
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: hire, milestone funding/approval, and
contract-end requests record a pending approval and return an approvalUrl
that a signed-in human must open and confirm in the OpenTrain app.
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.
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.
Quick start (agent onboarding — no human account needed)
# 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 --waitAlready 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
opentrain --version
opentrain whoami [--json]
opentrain auth register [--agent-name <name>] [--org-name <name>] [--base-url <url>] [--force] [--json]
opentrain auth claim --email <human-email> [--claim-token <token>] [--json]
opentrain auth claim-status [--wait] [--timeout <seconds>] [--claim-token <token>] [--json]
opentrain auth login --api-key <token> [--base-url <url>]
opentrain auth status [--json]
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 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> [--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 contracts list [--job-id <id>] [--status active|ended] [--limit <n>] [--cursor <cursor>] [--all] [--json]
opentrain contracts get --contract-id <id> [--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>] [--limit <n>] [--cursor <cursor>] [--json]
opentrain proposals get --proposal-id <id> [--interview] [--json]
opentrain proposals hire --proposal-id <id> --amount <usd> [--milestone-name <name>] [--milestone-description <text>] [--due-date <iso>] [--confirm-not-fit-override] [--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 instructions help
opentrain todos help
opentrain lms help
opentrain manager helpPass --json on any command for the exact API response body (best for agents
and scripts).
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
operating-guide digest — there is no second bootstrap command.
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 generated read-only Job Operating Guide (guideVersion + SHA-256 checksum). 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+, and the Work
Catalog, coordination modes, and external attestations need CLI 0.23.0+.
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> --json
opentrain instructions page get --job <job-id> --page <page-id> --json
opentrain instructions page link --job <job-id> --page <page-id> \
--anchor <stable-node-id> --json
opentrain instructions page preview --job <job-id> \
--content-file ./handbook.tiptap.json --json
opentrain instructions page create --job <job-id> --title "Reviewer handbook" \
--key reviewer-handbook-v1 --content-file ./handbook.tiptap.json \
--confirm-live --json
opentrain instructions import docx --job <job-id> --file ./handbook.docx \
--title "Reviewer handbook" --key reviewer-handbook-docx-v1 --confirm-live --json
opentrain instructions page patch --job <job-id> --page <page-id> \
--expected-checksum <sha256> --ops-file ./patch.json --confirm-live --json
opentrain instructions verify --job <job-id> --json
opentrain instructions visual-check --job <job-id> --json
# 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 --json
opentrain instructions diff --dir ./job-instructions --json
opentrain instructions validate --dir ./job-instructions --json
opentrain instructions plan --dir ./job-instructions --out ./job-instructions.plan.json --json
opentrain instructions apply --plan ./job-instructions.plan.json --confirm-live --json
opentrain instructions verify --job <job-id> --against-bundle ./job-instructions --jsonEvery 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> --json
# 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 --json
# Convert a supported YouTube URL to the canonical safe TipTap node offline.
opentrain instructions youtube-node --url "https://youtu.be/jNQXAC9IVRw" \
--title "Project walkthrough" --jsonFresh 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 roots plus one subpage level, matching the job editor. 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 5 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.
Draft 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> --json
opentrain instructions history show --job <job-id> --page <page-id> \
--checkpoint <checkpoint-id> --json
opentrain instructions history compare --job <job-id> --page <page-id> \
--checkpoint <checkpoint-id> --to current --json
opentrain instructions history checkpoint --job <job-id> --page <page-id> \
--key <idempotency-key> --json
opentrain instructions history restore --job <job-id> --page <page-id> \
--checkpoint <checkpoint-id> --expected-checksum <sha256> \
--key <fresh-idempotency-key> --confirm-restore --confirm-live --jsonListing 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> --json
opentrain instructions release preview --job <job-id> --candidate <candidate-id> --json
opentrain instructions release publish --job <job-id> --candidate <candidate-id> \
--operation-key <key> --delivery silent|notify|read-required --confirm --json
opentrain instructions release status --job <job-id> --jsonCandidates 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 and remain on
the existing immediately-live Instructions behavior.
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 --json
opentrain lms courses create --title "Reviewer onboarding" \
--origin-job <job-id> --key reviewer-onboarding-v1 --json
opentrain lms courses recover-create --key reviewer-onboarding-v1 --jsonLMS reads also return canonical human-navigation links[]. Text mode prints
them under Canonical links:; --json 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 --json
opentrain lms pull --dir ./course --json
opentrain lms reconcile --dir ./course --json
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 --json
opentrain lms assets list --course <course-id> --json
opentrain lms assets get --course <course-id> --asset <asset-id> --jsonPublishing, job linking, enabling Training, and assigning learners are separate confirmed actions. None silently performs another:
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
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-v1Native Forms assessments can be created, revision-fenced, published, bound to
lessons, graded, released, decided, and returned for another attempt. Run
opentrain lms assessments help and the focused leaf help for their exact JSON
schemas and mutation boundaries.
For individualized retraining, capture the QC evidence in a DRAFT case and run
opentrain lms retraining scaffold --case <case-id> --json. The scaffold is
strictly no-write: it returns the masked recipient, grounded outline, effective
authoring intent, deterministic capture hash, constraints, and the ordered
existing LMS/Native Forms/Preview/publish/assignment operations. It creates no
course, assessment, Preview, assignment, Project To-do, notification, or
attempt. Review and author the content before using the separate signed plan
and confirmed assignment commands.
Hosted visual proof renders the real worker player without creating learner progress. Rendering captures the session's current focus (target flags are assertions, not navigation); typed steps use a closed action grammar and return before/after evidence. Human review links are one-time-returned, short-lived, navigation-only, revocable, and require a signed-in employer organization member:
opentrain lms preview render --session <id> --expected-state-revision <n> \
--viewport desktop --viewport mobile --key <key> --output-dir ./evidence --json
opentrain lms preview step --session <id> --expected-state-revision <n> \
--action complete_content --viewport desktop --key <key> --json
opentrain lms preview share --session <id> --lesson <lesson-key> \
--expires-in 900 --key <key> --json
opentrain lms preview links revoke --session <id> --link <link-id> --key <key> --json
opentrain lms preview audit --session <id> --viewports desktop,mobile \
--out ./complete-worker-audit --key <stable-root-key> --jsonPNG, ARIA, diagnostics, and manifest artifacts are revision-bound and verified
for checksum, byte size, signature, and dimensions before success is reported.
The audit command walks the exact pinned course in canonical order, captures
assessment lessons before answers are supplied, supports safe per-target
retries, and publishes its local evidence directory only when the complete
course succeeds. It never creates learner progress, assignments, Project
To-dos, Forms attempts, or notifications.
Run each leaf with help for its exact schema and zero-write boundary.
Task Checklists (machine-first)
Adaptive per-job Task Checklist authoring with full web-builder parity
(job_checklists:read/write; default-off agent surface). The family is
machine-first: exactly one typed JSON envelope on stdout by default,
--pretty for humans. File-first workflow — the server owns every product
rule:
opentrain checklists init --dir ./checklist --title "Vehicle checks" --required
opentrain checklists validate --dir ./checklist --job-id <job-id> # no writes
opentrain checklists push --dir ./checklist --job-id <job-id> # three-way merge + CAS save
opentrain checklists publish --dir ./checklist --confirm-live # applied impact + sweep counts
opentrain checklists preview --dir ./checklist --open # secret rides the URL fragment
opentrain checklists submissions --job-id <job-id>
opentrain checklists enable --job-id <job-id> --key <k> # worker visibility (final step)Run opentrain checklists help for the full lifecycle, key policy
(reversible transitions require an explicit --key), and output contract.
Reviewer Feedback (employer-only, machine-first)
Job-scoped reviewer feedback (job_feedback:read/write; default-off
privacy-sensitive agent surface). Subjects never read feedback, author
identity, severity, notes, history, or counts through any agent surface.
EVERY mutation requires an explicit --key; triage carries the required
--expected-updated-at CAS token:
opentrain feedback settings --job-id <job-id> # works while the toggle is OFF
opentrain feedback enable --job-id <job-id> --confirm --key <k>
opentrain feedback roster --job-id <job-id> # canonical subject people + contracts
opentrain feedback submit --job-id <job-id> --subject-user-id <user-id> \
--severity MAJOR --suggested-action RETRAINING \
--report "Systematically mislabeled the calibration batch." --key <k>
opentrain feedback submit --job-id <job-id> --subject-user-id <user-id> \
--assessment POSITIVE --overall-performance EXCEEDS_EXPECTATIONS --key <k>
opentrain feedback list --job-id <job-id> --status OPEN --severity MAJOR
opentrain feedback list --job-id <job-id> --lane RECORDED --assessment POSITIVE
opentrain feedback triage --report-id <id> --to-status IN_REVIEW \
--expected-updated-at <updatedAtIso> --key <k>
opentrain feedback access grant --job-id <job-id> --user-id <user-id> --key <k>Balanced evaluations (DEV-3168): without --assessment the legacy shape
stays required exactly as before (severity + report; recorded as a
needs-improvement concern). With --assessment, severity present means a
concern is flagged — POSITIVE never takes severity/suggested action,
NEEDS_IMPROVEMENT requires severity, and any flagged concern requires the
report body. Optional --overall-performance / --knowledge anchored
ratings default to Not rated. Non-concern rows form the "Recorded" lane:
their stored severity MINOR / status OPEN are compatibility shadows, so
--lane RECORDED cannot combine with a concrete --status or
--severity (usage error locally; typed 400 on the server). Rating
aggregates are distributions/latest/rated-n only — never average, score,
or rank them.
Run opentrain feedback help for the full command set (summary, get,
link-subject, access revoke/bulk, disable) and the exact note/confirmation
rules.
Job drafting & publishing
Draft create is the primary workflow. Send a normal job description (inline or from a file); OpenTrain parses it server-side into structured job fields and returns an unpublished draft URL plus validation state:
cat > /tmp/job.txt <<'EOF'
We need experienced image annotation specialists to draw bounding boxes around
retail products in catalog photos. The dataset has about 1,200 JPG files.
Contributors should have prior image labeling or QA experience, write short
notes for edge cases, and report daily throughput. Pay is $25 per hour.
EOF
opentrain jobs draft create --description-file /tmp/job.txtWhen the draft is not publish ready, each missing field in the output includes
an ask: question to relay to the human, the value type, allowed enum values,
and the field name(s) to set. Fill gaps one field at a time:
opentrain jobs draft update --job-id <id> --set aiInterviewRequirements="Explain your QA process"Numbers, booleans, and JSON arrays are auto-coerced; everything else stays a string. Repeat until publish ready, then:
opentrain jobs publish --job-id <id>Publishing runs the same validation + moderation pipeline as the in-app flow
and is subject to per-account daily publish limits. Structured source systems
can use --canonical-file or --payload-file instead of a plain description.
Proposals, hiring, and messaging
# Rank candidates for a job (bid, status, AI-interview and match-score signals)
opentrain proposals list --job-id <id>
# Full candidate evaluation: bid, AI-interview score + summary, location and
# identity verification, Open Label assessment, and contract state.
# --interview appends the sanitized AI-interview transcript.
opentrain proposals get --proposal-id <id> --interview
# Masked freelancer profile by user id or public profile slug: skills, stats,
# work/label experience, education, reviews, languages. Names stay masked to
# first name + last initial; personal contact details (including personal
# email) are never returned.
opentrain freelancers get --id <user-id-or-slug>
# Read-only invitation state for an employer-owned job (DEV-2846): INVITED,
# DECLINED, APPLIED, or HIRED (hireAcceptancePending separates an
# authorized-but-unaccepted hire). No expiry is ever fabricated. Exactly one
# { ok, data } JSON envelope by default; --pretty for human lines; --all
# follows nextCursor and reports explicit collection metadata.
opentrain jobs invitations list --job-id <id> --state INVITED --all
opentrain jobs invitations get --job-id <id> --freelancer-id <id> --prettyjobs invitations list / get read canonical Joboffer rows on the
proposals:read scope inside the default-off proposal_management family
(AGENT_SURFACE_PROPOSAL_MANAGEMENT_ENABLED; a disabled family is a typed
403 AGENT_SURFACE_DISABLED): withheld (hidden, banned, or closed) profiles never
appear in a page and look up as invited: false with a null invitation, the
same shape as never invited. The --state filter is the exact uppercase enum
(INVITED|DECLINED|APPLIED|HIRED); lowercase, unknown, or invented values
(such as EXPIRED) are usage errors before any HTTP request.
Rate integrity (DEV-3025): every rate-bearing read (proposals list,
proposals get, freelancers get, talent search, dossier get, dossier
applications) carries a status next to the number — VALID, MISSING, or
INVALID_RATE_DATA (the stored value fell outside the canonical 1..1000 USD /
2-decimal shape, so the number is null rather than corrupt). Human output
prints Rate unavailable (INVALID_RATE_DATA) for that case; n/a still means
"not provided". JSON output is the exact server projection.
One identity verdict across every read
proposals get, freelancers get, and dossier get return the SAME
structured identity verdict for the same candidate at the same time: explicit
status (VERIFIED, REQUESTED, IN_PROGRESS, FAILED, EXPIRED,
NEEDS_REVIEW, CANCELED, REQUIRED_NOT_REQUESTED, NOT_REQUESTED,
UNKNOWN), derived
verified/current, provider, country, timestamps, an asOf freshness
anchor, source (canonical record vs legacy Stripe), and an explicit
conflict object when sources disagree. For hiring and onboarding
decisions, the structured verdict (verification.identity /
identityVerification) is authoritative; top-level identityVerified
booleans are derived compatibility fields and can never contradict it.
UNKNOWN means the signal was unavailable — never treat it as "not
verified". Proposal-time location, current location, and project eligibility
are separately scoped facts, not identity.
Candidate dossier (bounded evidence)
# One-call proposal-linked, viewer-scoped dossier (concise by default)
opentrain dossier get --proposal-id <id>
# Auditable mode: adds ten bounded metadata-only evidence families
opentrain dossier get --proposal-id <id> --evidence full --json
# Audited resume access (DEV-2977 B3.5): fresh 600-second signed URL when
# AVAILABLE, truthful typed states otherwise. Requires candidate_documents:read
# IN ADDITION to candidate_dossier:read; every access is audited append-only.
opentrain dossier resume --proposal-id <id> --json
# Pageable lists behind the bounded previews
opentrain dossier reviews --proposal-id <id> --limit 10
opentrain dossier contract-history --proposal-id <id> --limit 10
# PLATFORM ADMIN ONLY (DEV-2887): one candidate's cross-organization
# application history from canonical Joboffer rows. Exactly one { ok, data }
# JSON envelope by default; --pretty for human lines; --all collects every
# page inside the original asOf snapshot and reports collection metadata.
opentrain dossier applications --user-id <id> --limit 50
opentrain dossier applications --user-id <id> --all --prettydossier resume is LINK-ONLY on the agent surface: no mode flag exists,
the request pins ?mode=link, redirects are never followed, and no bytes
are downloaded. Download the file with your own client from the returned
signed URL (curl -L "<url>" -o resume.pdf), or follow the dossier's
stable links.resume.url (…/dossier/resume?mode=redirect) which 302s to
a fresh signed URL — candidate self-access is refused with a typed
SELF_ACCESS_NOT_SUPPORTED denial.
Resume provenance (DEV-3024) travels with the link as five correlated
variants. When availability is AVAILABLE, provenanceStatus is either
AVAILABLE with a provenance object — storage-observed fileName,
contentType, sizeBytes, uploadedAt, lastModifiedAt, an observedAt
server clock, a checksum ({ algorithm: "SHA256", value }) only when
storage recorded one (null today), and the opaque storageEtag (an HTTP
validator, NOT a checksum) — or UNAVAILABLE with provenance: null when
the metadata read failed while the signed URL stayed valid. Every
unavailable link state carries provenanceStatus: NOT_APPLICABLE and
provenance: null. Nothing is parsed from the file and no storage path,
bucket, or version identifier is ever returned.
dossier applications is a PLATFORM ADMIN ONLY read behind the same
candidate_dossier:read scope: PROPOSAL and INVITE rows are returned and
counted SEPARATELY, counts and velocity cover the whole history as of
asOf (not only the current page), bid amounts appear only behind the admin
gate with a rate-integrity status, and cover letters, contact details,
messages, assessments, and private employer review notes are never returned
(redaction.excluded). Non-admins and unknown candidates read one uniform
non-enumerating 404. Pages are bound to the original asOf, so --all
collects a single consistent snapshot.
# Audited participant work email (DEV-3031 B3.6): the managed
# @opentrain.work address for a worker you manage — exactly one selector.
opentrain participant work-email --contract-id <id> --json
opentrain participant work-email --proposal-id <id>
opentrain participant work-email --job-id <id> --person-user-id <id>participant work-email requires the dedicated participant_work_email:read
scope and returns ONLY the OpenTrain-managed address — never the personal
signup/login email. The selector grammar is exact by flag presence: a bare
user id is never accepted, mixed selector flags fail even with empty values,
and every violation is a usage error with zero HTTP. ONLY the ACTIVE state
carries an address; ended contracts read as typed CONTRACT_ENDED, un-hired
proposals as typed NOT_HIRED; every authenticated attempt is audited
append-only server-side BEFORE any address is disclosed.
--evidence is a strict lowercase enum: concise (default — the response's
evidenceMode is CONCISE and evidence is null) or full. An empty,
uppercase, whitespace-wrapped, unknown, or repeated value — in either the
split or --evidence= form — is a deterministic usage error before any HTTP
request, never a silent downgrade. Full mode adds ten bounded metadata-only
families (identity verification history, identity requests, eligibility
decisions, location observations, proposal and contract facts, a
contract-history preview, work-assessment attempts, curated reviews, and
proposal-DM activity metadata), each capped at 25 items with a normalized
totalCount, a truthful truncated flag, and a typed continuation
(COMPLETE, BOUNDED_SNAPSHOT_ONLY, or OPERATION + cursor into
dossier reviews / dossier contract-history). Evidence and engagement
history are viewer-scoped: an employer or delegate read covers the
principal employer's own history with the candidate
(engagement.scope: "EMPLOYER_CANDIDATE"), while a candidate reading their
own dossier sees only the current proposal's job
(engagement.scope: "CURRENT_PROPOSAL_JOB"). Evidence never contains
message bodies or participants, contact details, raw resume files or URLs,
government identity documents, provider/session references, assessment
contents, compensation amounts in cross-buyer history, or
coordinates/IP/ASN/timezone data. The human summary prints per-family
counts; the complete structured evidence block requires --json.
# Hire request: does NOT hire or move money. Records a pending approval and
# returns an approvalUrl a signed-in human must open and confirm in the
# OpenTrain app (~72h expiry). Requires a claimed account + payment method or
# covering credit balance — a 409 response includes a billingUrl for the human.
opentrain proposals hire --proposal-id <id> --amount 500
# Open the pre-hire DM thread for a proposal
opentrain messages start-proposal-thread --proposal-id <id>
# Invite a freelancer to a published job
opentrain jobs invite --job-id <id> --freelancer-id <id>
# Conversations and messages
opentrain messages list --filter all --limit 20
opentrain messages read --conversation-id <id> --limit 50
opentrain messages send --conversation-id <id> --content "Thanks — looks great."Message reads are read-only: they do not create conversations, mark messages read, or return raw attachment URLs. Sending requires a claimed account.
Talent discovery (search & reputation)
# Hybrid ranked search over the vetted talent pool (semantic + lexical + fuzzy)
opentrain talent search --q "rubric writing finance" --available-now --limit 20 --json
# Facet browse (no --q): reputation-ranked listing filtered by facets only
opentrain talent search --skill accounting --country PH --tier PROVEN
# Aggregate reputation card for one profile
opentrain talent reputation --slug <profile-slug-or-user-id>With --q, results rank by hybrid relevance (semantic + full-text + fuzzy
match over titles, skills, and bios). Without --q, the command is a facet
browse ordered by reputation. --query is an alias for --q, and bare
positional words are treated as the query.
All filters compose with each other and with --q: --skill, --language,
--country <ISO>, --badge VERIFIED_TRAINER,
--talent-type Individual|Agency, --availability <text> (freeform facet),
--min-reputation <n>, --tier PROVEN|ESTABLISHED|PROVISIONAL (alias
--confidence-tier), and --available-now (only people with no current
assignment load). Sorting: --sort relevance|reputation|rate_asc|rate_desc|recent
— the default is relevance with --q and reputation order without it.
Pagination: --limit accepts 1..50 (default 25) plus --offset. Invalid
parameter shapes return 400 BAD_REQUEST with the offending parameter named
in details rather than being silently coerced.
Results are privacy-masked aggregate talent cards: first name + last initial,
no contact details, no documents, no cross-buyer private assessments.
Representative --json response:
{
"results": [
{
"card": {
"userId": "cm…",
"slug": "ada-l",
"displayName": "Ada L.",
"title": "Finance & accounting AI evaluator",
"countryCode": "DE",
"hourlyRateUsd": 30,
"reputation": { "compositeScore": 91.4, "confidenceTier": "ESTABLISHED" },
"badges": []
},
"score": 0.87
}
],
"total": 1,
"limit": 20,
"offset": 0,
"generatedAt": "2026-08-29T00:00:00.000Z"
}Contracts and milestones (co-signed money movement)
# Contract status, milestones with funding/approval state, budget consumption,
# and the post-hire job DM conversationId. The global list (no --job-id) is a
# bounded page (DEV-2839): default 50, max 100, newest first, with nextCursor.
opentrain contracts list --status active
opentrain contracts list --status active --limit 100 --cursor <nextCursor>
# Collect every page at a valid page size with truthful collection metadata
# (pagesFetched, collectedCount, complete, finalCursor, stopReason). --all with
# --job-id pages the job-scoped read too; without --limit/--cursor/--all a
# job-scoped read keeps the legacy unpaged list.
opentrain contracts list --status active --all --json
opentrain contracts get --contract-id <id>
# Add an unfunded milestone directly (no money moves)
opentrain milestones create --contract-id <id> --description "Batch 2: 5,000 labels" --amount 250
# Funding and approval record a pending approval and return an approvalUrl —
# a signed-in human confirms in the OpenTrain app before money is held or released
opentrain milestones fund --milestone-id <id>
opentrain milestones approve --milestone-id <id>
opentrain approvals get --approval-id <id>
# Ending a contract succeeds only after every milestone is completed,
# cancelled, or successfully refunded; 409 responses list blockers and actions
opentrain contracts end --contract-id <id>Updates feed and webhooks
# Cursor-based delta feed: new proposals, messages, contract/milestone
# changes, approval confirmations, budget state changes. Persist the returned
# nextCursor and pass it on the next poll.
opentrain updates poll --cursor <nextCursor>
# Or push: signed HMAC webhook deliveries of the same events. The signing
# secret is shown once at creation.
opentrain webhooks create --url https://example.com/hooks/opentrain --events proposal.created,approval.confirmed
opentrain webhooks listCredits (prepaid balance)
opentrain credits show
opentrain credits ledger
# Returns a Stripe Checkout URL a human completes — the agent cannot pay
opentrain credits top-up --amount 500
opentrain credits top-up-status --top-up-id <id>Team and payments
opentrain team show
opentrain team invite --email [email protected]
opentrain payments pending
opentrain payments verify --invoice-id <invoice-id>
opentrain payments recovery-preflight --invoice-id <invoice-id> [--approval-id <approval-id>]
opentrain tokens listpayments pending and payments verify are read-only. payments verify
retrieves current provider state and reports reversals or disagreement with
OpenTrain. payments recovery-preflight additionally searches every matching
Stripe transfer page and refuses to call a manual replacement safe when an
unreversed equivalent already exists. None of these commands can fund,
approve, refund, release, reconcile, or move money.
Configuration
- Saved config:
~/.config/opentrain/cli.json(or$XDG_CONFIG_HOME/opentrain/cli.json), shared with the OpenTrain MCP server (@opentrain-ai/mcp) — registering through either surface makes the credentials available to both. - Env overrides:
OT_API_TOKEN/OPENTRAIN_API_TOKENandOT_API_BASE_URL/OPENTRAIN_API_BASE_URL. - Default base URL:
https://app.opentrain.ai.
Reliability
Requests time out after 30 seconds (publish and hire get 120 seconds). The CLI automatically retries transient failures with exponential backoff (up to 3 attempts): reads (GET) retry on 502/503/504 gateway errors and network failures; writes retry only on errors that occur before the request reaches the server (connection refused, DNS failures), so a write is never sent twice.
Exit codes and JSON error envelope
Every non-2xx API response exits nonzero; automation must check the exit
status, not the presence of output. When an OpenTrain API request fails and
--json was passed, the CLI prints exactly one JSON envelope on stdout
(instead of the human-readable stderr lines) and still exits 1:
{
"ok": false,
"error": {
"code": "INTERNAL_ERROR",
"status": 500,
"message": "Failed to search talent",
"requestId": "req_abc123",
"details": { "reason": "..." }
}
}code is always present: the server's machine code when it returned an error
envelope, or the stable fallback HTTP_ERROR when it did not. requestId is
taken from the error envelope or the x-request-id response header and is
null only when neither provided one. details is present only when the
server returned structured details. The envelope carries server-provided
fields only — never tokens, headers, or stack traces.
Transient failures and concurrency
When a platform dependency behind authentication is briefly unavailable, the
API returns a retryable 503 AUTHORIZATION_UNAVAILABLE envelope with a
Retry-After header and a requestId instead of a terminal error. The CLI's
standard retry (3 attempts on 502/503/504 for GET/HEAD reads, exponential
backoff) absorbs these automatically. The write-safety boundary is exact:
writes retry only on pre-connection failures (connection refused, DNS) where
the request provably never reached the server — a write HTTP response or an
ambiguous mid-flight failure is never retried, so a write is never sent
twice. Terminal 401/403 responses are attempted exactly once. Modest
concurrent read batches (for example four parallel talent search calls)
are a supported envelope; each request retries independently.
Quote the requestId when reporting a failure so operators can find the exact
server-side request.
Development (monorepo)
npm install
npm run build -w @opentrain-ai/cli
npm exec -w @opentrain-ai/cli opentrain -- whoami