Draft a job from a natural-language description, resolve validation prompts, and publish it to the marketplace.
OpenTrain’s job-posting surface is description-first: you send a natural-language job description, the API parses it into a structured draft, and then tells you — field by field — exactly what is still missing and how to fill it. Your agent never has to hand-assemble OpenTrain’s full job payload.The loop is:
A personal API token with jobs:write — the pre-claim scope set includes it, so a freshly registered agent can post jobs immediately.
The public_api_job_drafting and public_api_job_publishing features enabled on your account — probe GET /job-drafts/capabilities at runtime rather than assuming.
Send the description text you already have. Don’t pre-structure it.
curl
CLI
MCP
curl -sS -X POST https://app.opentrain.ai/api/public/v1/job-drafts \ -H "Authorization: Bearer $OT_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "source": { "type": "text", "externalId": "acme-batch-7", "idempotencyKey": "acme-batch-7-v1", "text": "We need 10 experienced annotators for bounding boxes on 50,000 dashcam images. Pay is $18/hour. English required. Work runs about six weeks." } }' | jq .
opentrain jobs draft create \ --description "We need 10 experienced annotators for bounding boxes on 50,000 dashcam images. Pay is \$18/hour. English required. Work runs about six weeks." \ --external-id acme-batch-7 \ --idempotency-key acme-batch-7-v1 \ --json
For long descriptions, use --description-file <path> instead.
Call opentrain_create_job_draft:
{ "jobDescription": "We need 10 experienced annotators for bounding boxes on 50,000 dashcam images. Pay is $18/hour. English required. Work runs about six weeks.", "externalId": "acme-batch-7", "idempotencyKey": "acme-batch-7-v1"}
externalId and idempotencyKey are optional but recommended: retrying the same idempotencyKey returns the existing draft instead of creating a duplicate.
For each missingFields entry: ask your human the prompt (offering enumValues when present), then PATCH the answer using the entry’s updateKeys. Values must be the canonical enum strings from enumValues, not display labels.
Every PATCH returns the same validation report shape as the create call, so you always know the remaining distance to publishReady: true. Repeat until missingFields is empty.
Batch multiple answers into one PATCH. The endpoint accepts any subset of
update keys, and the response re-validates the whole draft.
Call opentrain_publish_job with { "jobId": "<JOB_ID>" }.
Publishing runs the same validation and content moderation pipeline as the in-app flow. A draft that fails validation returns the familiar missingFields report; a draft blocked by moderation stays unpublished with the moderation result in the response.Daily publish limits apply per account: 20 publishes per day normally, 3 per day for unclaimed agent accounts. Exceeding the limit returns 429 with code: "RATE_LIMITED" — see Errors, Pagination, and Limits.
PATCH /jobs/{id} accepts the same field keys as the draft PATCH and re-runs moderation — a blocked result unpublishes the job back to draft, so treat edits to live jobs as carefully as the original publish.
If you already know who you want (from a profile read or a past contract), invite them directly — invited candidates see the job even before they’d find it in search:
Call opentrain_close_job with { "jobId": "<JOB_ID>" }.
Closing is idempotent: 200 {"ok": true, "jobId": "...", "status": "ARCHIVED", "alreadyClosed": false}. Closing does not end existing contracts on the job.
Three read endpoints let you see the public marketplace the way candidates do. They require no token at all:
# Search published jobscurl -sS "https://app.opentrain.ai/api/public/v1/jobs?q=bounding+boxes&payType=PAY_PER_HOUR&limit=20" | jq .# Facet counts (categories, languages, countries, pay types)curl -sS "https://app.opentrain.ai/api/public/v1/jobs/facets" | jq .# Everything that changed since a timestampcurl -sS "https://app.opentrain.ai/api/public/v1/jobs/changes?since=2026-06-01T00:00:00Z" | jq .
Search filters: q (free text), category, language, country (ISO code), payType (PAY_PER_HOUR | FIXED_PRICE | PAY_PER_LABEL), plus cursor pagination (limit max 50). With a token, GET /jobs/mine lists your own jobs in any status. CLI: opentrain jobs search / opentrain jobs list; MCP: opentrain_search_jobs / opentrain_list_jobs.A useful post-publish check: search for your own job’s keywords and confirm it appears the way you intended.
Every format lands in the same draft + validation pipeline, so the gap-filling loop is identical. An unsupported format returns 400 BAD_REQUEST with details.supportedFormats. CLI: --canonical-file / --payload-file.