> ## Documentation Index
> Fetch the complete documentation index at: https://opentrain.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Create Job Draft

> Create a job draft from a natural-language description and receive validation prompts for missing fields.

Creates an unpublished job draft. The description-first workflow: send a plain-text job description, OpenTrain parses it into structured fields server-side, and the response tells you exactly what is still missing — each missing field carries a ready-to-ask `prompt`, its `type`, allowed `enumValues`, and the `updateKeys` to patch via [`PATCH /job-drafts/{jobId}`](/docs/developers/api-reference/job-drafts/update). Loop until `validation.publishReady` is `true`, then [publish](/docs/developers/api-reference/jobs/publish).

Structured imports (schema.org JSON-LD, Indeed-style XML, HR-XML, or OpenTrain's canonical job object) are also accepted. See the [posting guide](/docs/developers/guides/post-a-job) for the full loop.

**Requirements:** `jobs:write` scope + the `public_api_job_drafting` feature (check [capabilities](/docs/developers/api-reference/job-drafts/capabilities)).

## Request

Send one of the body shapes below. All are JSON objects.

<ParamField body="rawJobDescription" type="string">
  Plain-text job description or project brief (max 60,000 characters). The simplest and recommended input — OpenTrain parses it into structured fields. Equivalent shorthand for `source: {type: "text", text: ...}`.
</ParamField>

<ParamField body="source" type="object">
  Structured import source.

  <Expandable title="source fields">
    <ParamField body="source.type" type="string">
      Import format: `text`, `opentrain_canonical`, `schema_org_job_posting`, `indeed_xml`, or `hr_xml`. Unsupported values return `400` with `details.supportedFormats`.
    </ParamField>

    <ParamField body="source.text" type="string">
      The text or serialized payload for the chosen format.
    </ParamField>

    <ParamField body="source.externalId" type="string">
      Source-system identifier for audit (max 256 characters).
    </ParamField>

    <ParamField body="source.idempotencyKey" type="string">
      Reuse the same key on retries to avoid duplicate drafts (max 256 characters).
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="format" type="string">
  Top-level alternative to `source.type` — same supported values. Pair with `job` (canonical), `jsonLd` (schema.org), or `xml` (feeds).
</ParamField>

<ParamField body="job" type="object">
  OpenTrain canonical job object when `format` is `opentrain_canonical` — keys like `title`, `description`, `paymentType`, `rateAmount`, `languages`, `countries`, `labelTypes`, `tools`, `experienceLevel`.
</ParamField>

<ParamField body="externalId" type="string">
  Top-level equivalent of `source.externalId`.
</ParamField>

<ParamField body="idempotencyKey" type="string">
  Top-level equivalent of `source.idempotencyKey`.
</ParamField>

## Response

<ResponseField name="ok" type="boolean">
  `true` on success.
</ResponseField>

<ResponseField name="jobId" type="string">
  The new draft's job ID — use it for every subsequent PATCH and the publish call.
</ResponseField>

<ResponseField name="status" type="string">
  Always `DRAFT` (this endpoint never auto-publishes).
</ResponseField>

<ResponseField name="draftUrl" type="string">
  In-app URL of the draft editor.
</ResponseField>

<ResponseField name="reviewUrl" type="string">
  In-app URL where a human can review the draft.
</ResponseField>

<ResponseField name="validation" type="object">
  Publish-readiness summary.

  <Expandable title="validation fields">
    <ResponseField name="publishReady" type="boolean">
      `true` when the draft can be published as-is.
    </ResponseField>

    <ResponseField name="issueCount" type="number">
      Number of blocking issues.
    </ResponseField>

    <ResponseField name="missingFieldCount" type="number">
      Number of required fields still missing.
    </ResponseField>

    <ResponseField name="missingFields" type="object[]">
      Same entries as the top-level `missingFields`.
    </ResponseField>

    <ResponseField name="issues" type="object[]">
      Non-field-specific validation issues.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="missingFields" type="object[]">
  The gap-filling work list. Relay each `prompt` to your human, then patch the answer.

  <Expandable title="missingFields entry">
    <ResponseField name="field" type="string">
      Internal field identifier.
    </ResponseField>

    <ResponseField name="label" type="string">
      Human-readable field name.
    </ResponseField>

    <ResponseField name="message" type="string">
      Why the field is required.
    </ResponseField>

    <ResponseField name="code" type="string">
      Machine-readable issue code.
    </ResponseField>

    <ResponseField name="prompt" type="string">
      A ready-to-ask question (e.g. `"ask: What experience level should AI trainers have?"`).
    </ResponseField>

    <ResponseField name="type" type="string">
      Expected answer type — `string`, `number`, `enum`, `string[]`, etc.
    </ResponseField>

    <ResponseField name="enumValues" type="string[] | null">
      Allowed values when `type` is `enum`. Patch with one of these exact values.
    </ResponseField>

    <ResponseField name="updateKeys" type="string[]">
      The key(s) to set in the PATCH body (e.g. `["paymentType", "pricePerHour"]`).
    </ResponseField>

    <ResponseField name="example" type="string | null">
      Example value.
    </ResponseField>

    <ResponseField name="hint" type="string | null">
      Extra guidance for answering.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="normalizedFields" type="object">
  The structured fields OpenTrain extracted from your input.
</ResponseField>

<ResponseField name="warnings" type="string[]">
  Non-blocking warnings.
</ResponseField>

<ResponseField name="unmappedFields" type="object[]">
  Input keys that could not be mapped — `{path, reason, valuePreview}`.
</ResponseField>

<ResponseField name="lowConfidenceFields" type="object[]">
  Parsed fields worth double-checking with your human — `{path, reason}`.
</ResponseField>

<ResponseField name="import" type="object">
  Audit echo: `{format, externalId, idempotencyKey, rawSourcePreserved, autoPublished: false}`.
</ResponseField>

<ResponseField name="parser" type="object">
  Parser metadata: `{source, warnings, parsedFields}`.
</ResponseField>

## Errors

| Status | `code`         | Meaning                                                                                                            |
| ------ | -------------- | ------------------------------------------------------------------------------------------------------------------ |
| `400`  | `BAD_REQUEST`  | Empty body, invalid JSON, unsupported format (`details.supportedFormats`), or description over 60,000 characters   |
| `401`  | `UNAUTHORIZED` | Missing or invalid token                                                                                           |
| `403`  | `FORBIDDEN`    | Missing `jobs:write` scope (`details.requiredScopes`) or `public_api_job_drafting` disabled (`details.featureKey`) |

<RequestExample>
  ```bash curl theme={null}
  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 '{
      "rawJobDescription": "We need 5 fluent Spanish speakers to label sentiment in ~20,000 social media posts. Pay per label. Annotators should have prior text-classification experience.",
      "idempotencyKey": "sentiment-es-2026-001"
    }'
  ```

  ```bash CLI theme={null}
  opentrain jobs draft create \
    --description "We need 5 fluent Spanish speakers to label sentiment in ~20,000 social media posts. Pay per label." \
    --idempotency-key sentiment-es-2026-001 --json
  ```

  ```json MCP: opentrain_create_job_draft theme={null}
  {
    "jobDescription": "We need 5 fluent Spanish speakers to label sentiment in ~20,000 social media posts. Pay per label.",
    "idempotencyKey": "sentiment-es-2026-001"
  }
  ```
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "ok": true,
    "jobId": "<JOB_ID>",
    "status": "DRAFT",
    "draftUrl": "https://app.opentrain.ai/job-post?jobId=<JOB_ID>",
    "reviewUrl": "https://app.opentrain.ai/job-post?jobId=<JOB_ID>",
    "validation": {
      "publishReady": false,
      "issueCount": 0,
      "missingFieldCount": 2,
      "missingFields": [],
      "issues": []
    },
    "missingFields": [
      {
        "field": "experienceLevel",
        "label": "Experience level",
        "message": "Experience level is required before publishing.",
        "code": "missing_required_field",
        "prompt": "ask: What experience level should AI trainers have? (EXPERT, ENTRY_LEVEL, INTERMEDIATE, ANY_EXPERIENCE_LEVEL)",
        "type": "enum",
        "enumValues": ["EXPERT", "ENTRY_LEVEL", "INTERMEDIATE", "ANY_EXPERIENCE_LEVEL"],
        "updateKeys": ["experienceLevel"],
        "example": "INTERMEDIATE",
        "hint": "Pick the minimum level the work realistically needs."
      },
      {
        "field": "pricePerLabel",
        "label": "Price per label",
        "message": "A pay rate is required before publishing.",
        "code": "missing_required_field",
        "prompt": "ask: How much will you pay per label (USD)?",
        "type": "number",
        "enumValues": null,
        "updateKeys": ["pricePerLabel"],
        "example": "0.05",
        "hint": null
      }
    ],
    "normalizedFields": {
      "jobTitle": "Spanish Sentiment Labeling — Social Media Posts",
      "paymentType": "PAY_PER_LABEL",
      "languages": ["Spanish"],
      "headcount": 5,
      "dataVolume": 20000,
      "dataVolumeUnit": "NUMBER_OF_FILES"
    },
    "warnings": [],
    "unmappedFields": [],
    "lowConfidenceFields": [
      { "path": "dataVolumeUnit", "reason": "Inferred NUMBER_OF_FILES from \"posts\"." }
    ],
    "import": {
      "format": "text",
      "externalId": null,
      "idempotencyKey": "sentiment-es-2026-001",
      "rawSourcePreserved": true,
      "autoPublished": false
    },
    "parser": {
      "source": "llm",
      "warnings": [],
      "parsedFields": 6
    }
  }
  ```

  ```json 400 (unsupported format) theme={null}
  {
    "error": "Unsupported job import format",
    "code": "BAD_REQUEST",
    "requestId": "<REQUEST_ID>",
    "details": {
      "supportedFormats": [
        "text",
        "opentrain_canonical",
        "schema_org_job_posting",
        "indeed_xml",
        "hr_xml"
      ]
    }
  }
  ```
</ResponseExample>
