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

# Train an Adapter

> Train a policy-specific adapter in Training Studio, then enforce against it.

You have imported and activated a policy. Extraction gives Anchor the letter of the policy. Training Studio trains a policy-specific adapter on ZeroDrift's base so enforcement can apply that policy's judgment, not only the extracted rule list. Anchor handles regulations. Your adapter handles your policies.

Train when the policy is nuanced, when extracted rules alone over-escalate, or when you want a single model path for regulations plus your policy. You do **not** have to train for custom rules to run — [Activate Rules](/api-reference/custom-policies/activate-rules) is enough for extracted rules.

## Terms

| Term             | Meaning                                                                                                              |
| ---------------- | -------------------------------------------------------------------------------------------------------------------- |
| **Policy**       | The imported document and the rules extracted from it.                                                               |
| **Adapter**      | The policy-specific model trained from that import. After a successful training poll, scoped enforcement can use it. |
| **Training run** | One Training Studio job for an import, identified by `training_run_id`.                                              |

## Prerequisites

* Full-access API key (read-only returns `403`).
* An import whose rules are **activated** (pending review returns `409`).
* The original document still within retention — **7 days** by default (`410` if expired).
* No other training run in progress for this import (`409`).
* Training enabled for your environment (`503` if not).

Set `IMPORT_ID` to the activated import id from [Import a Policy](/guides/import-a-policy).

## Flow

```
train → poll until succeeded|failed → enforce with validation_scope.imports
```

## Start, poll, enforce

Each sample starts training, polls to a terminal status, then enforces with `validation_scope.imports`. Omit the POST body to use Training Studio defaults, or set `cost_cap_usd` and `examples_per_rule`.

Do not enforce against a still-training run. ZeroDrift promotes the adapter only after a successful status poll. Until then, scoped enforcement follows its configured fallback path. A failed retrain does not replace an earlier working adapter.

<CodeGroup>
  ```bash cURL theme={null}
  IMPORT_ID="${IMPORT_ID:?set IMPORT_ID to an activated import}"

  curl -fsS -X POST "https://api.zerodrift.ai/api/policies/import/$IMPORT_ID/train" \
    -H "x-api-key: $ZERODRIFT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"cost_cap_usd": 25, "examples_per_rule": 8}'

  # Poll until training_status or status is succeeded or failed
  while true; do
    RESP=$(curl -fsS "https://api.zerodrift.ai/api/policies/import/$IMPORT_ID/train" \
      -H "x-api-key: $ZERODRIFT_API_KEY")
    STATUS=$(printf '%s' "$RESP" | jq -r '.training_status // .status')
    echo "$STATUS"
    case "$STATUS" in
      succeeded|failed) break ;;
      queued) ;;
      *) printf '%s\n' "$RESP" >&2; exit 1 ;;
    esac
    sleep 10
  done

  printf '%s\n' "$RESP"
  test "$STATUS" = "succeeded"

  curl -X POST "https://api.zerodrift.ai/api/v3/content/validate" \
    -H "x-api-key: $ZERODRIFT_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{
      \"content\": \"Our fund guarantees 20% returns with zero risk!\",
      \"model_engine\": \"anchor_3_0\",
      \"mode\": \"sync\",
      \"document_category\": \"scenario_retail_investor_letter\",
      \"validation_scope\": {\"imports\": [\"$IMPORT_ID\"]}
    }"
  ```

  ```python Python theme={null}
  import os
  import time

  import requests

  API = "https://api.zerodrift.ai"
  HEADERS = {
      "x-api-key": os.environ["ZERODRIFT_API_KEY"],
      "Content-Type": "application/json",
  }
  IMPORT_ID = os.environ["IMPORT_ID"]
  URL = f"{API}/api/policies/import/{IMPORT_ID}/train"

  started = requests.post(
      URL,
      headers=HEADERS,
      json={"cost_cap_usd": 25, "examples_per_rule": 8},
      timeout=30,
  )
  started.raise_for_status()
  print(started.json())

  while True:
      training = requests.get(URL, headers=HEADERS, timeout=30)
      training.raise_for_status()
      body = training.json()
      status = body.get("training_status") or body.get("status")
      print(status, body.get("stage"))
      if status in ("succeeded", "failed"):
          break
      time.sleep(10)

  if (body.get("training_status") or body.get("status")) == "failed":
      raise SystemExit(body.get("error", "Training failed"))

  enforced = requests.post(
      f"{API}/api/v3/content/validate",
      headers=HEADERS,
      json={
          "content": "Our fund guarantees 20% returns with zero risk!",
          "model_engine": "anchor_3_0",
          "mode": "sync",
          "document_category": "scenario_retail_investor_letter",
          "validation_scope": {"imports": [IMPORT_ID]},
      },
      timeout=30,
  )
  enforced.raise_for_status()
  job = enforced.json()
  if job.get("mode") == "async":
      poll = job["poll"]["url"]
      while True:
          job = requests.get(f"{API}{poll}", headers=HEADERS, timeout=30).json()
          if job.get("status") in ("completed", "done", "failed"):
              break
          time.sleep(2)
  print(job.get("status"), (job.get("result") or job).get("overall_status"), job.get("served_by_adapter"))
  ```

  ```typescript TypeScript theme={null}
  const API = "https://api.zerodrift.ai";
  const HEADERS = {
    "x-api-key": process.env.ZERODRIFT_API_KEY!,
    "Content-Type": "application/json",
  };
  const IMPORT_ID = process.env.IMPORT_ID!;
  const URL = `${API}/api/policies/import/${IMPORT_ID}/train`;

  const started = await fetch(URL, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({ cost_cap_usd: 25, examples_per_rule: 8 }),
  });
  if (!started.ok) throw new Error(`train ${started.status}`);
  console.log(await started.json());

  let body: {
    status?: string;
    training_status?: string;
    stage?: string;
    error?: string | null;
  };
  for (;;) {
    const r = await fetch(URL, { headers: HEADERS });
    if (!r.ok) throw new Error(`status ${r.status}`);
    body = await r.json();
    const status = body.training_status ?? body.status;
    console.log(status, body.stage);
    if (status === "succeeded" || status === "failed") break;
    await new Promise((res) => setTimeout(res, 10_000));
  }

  if ((body.training_status ?? body.status) === "failed") {
    throw new Error(body.error ?? "Training failed");
  }

  const enforced = await fetch(`${API}/api/v3/content/validate`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({
      content: "Our fund guarantees 20% returns with zero risk!",
      model_engine: "anchor_3_0",
      mode: "sync",
      document_category: "scenario_retail_investor_letter",
      validation_scope: { imports: [IMPORT_ID] },
    }),
  });
  if (!enforced.ok) throw new Error(`enforce ${enforced.status}`);
  let job = await enforced.json();
  if (job.mode === "async") {
    const pollUrl = job.poll.url;
    for (;;) {
      const polled = await fetch(`${API}${pollUrl}`, { headers: HEADERS });
      job = await polled.json();
      if (["completed", "done", "failed"].includes(job.status)) break;
      await new Promise((res) => setTimeout(res, 2000));
    }
  }
  const verdict = job.result ?? job;
  console.log(job.status, verdict.overall_status, job.served_by_adapter);
  ```
</CodeGroup>

## Errors

| Status | Cause                                                | Fix                                                                  |
| ------ | ---------------------------------------------------- | -------------------------------------------------------------------- |
| `400`  | Invalid training options                             | Check `cost_cap_usd` and `examples_per_rule`.                        |
| `403`  | Read-only key, or import belongs to another customer | Use a full-access key for the owning account.                        |
| `404`  | Import not found, or no run started (GET)            | Confirm `IMPORT_ID`; POST before you poll.                           |
| `409`  | Import not activated, or a run already in progress   | Activate rules; wait for the current run.                            |
| `410`  | Original document expired                            | Re-import, activate, then train ([Data Retention](/data-retention)). |
| `422`  | Document too short or rejected                       | Provide a fuller policy document.                                    |
| `502`  | Training Studio request failed                       | Retry; contact support if it persists.                               |
| `503`  | Training not configured                              | Contact [support@zerodrift.ai](mailto:support@zerodrift.ai).         |

## Gotchas

* One training run per import at a time. A second POST while a run is active returns `409`.
* Retrain after you materially change the policy (re-sync or re-import, activate, train again).
* Adapters apply when enforcement includes that import in `validation_scope.imports`.
* Live GET responses report `training_status` (`queued`, `succeeded`, `failed`). Some responses also include `status`.
* A failed retrain does not replace an earlier working adapter.

## FAQ

<AccordionGroup>
  <Accordion title="Do I have to train before custom rules run?">
    No. [Activate Rules](/api-reference/custom-policies/activate-rules) puts extracted rules into enforcement immediately. Training Studio adds a policy-specific adapter on top of that.
  </Accordion>

  <Accordion title="Can I train a document I synced from Notion or Drive?">
    Yes. A connected-source import is the same `import_id` as a file upload. Activate it, then call the train endpoints. See [MCP Connectors](/connecting-mcp).
  </Accordion>

  <Accordion title="Why did training return 410?">
    Training needs the original imported file or text. After default [content retention](/data-retention) (7 days unless your contract says otherwise), that document is deleted. Import again, activate the new rules, then train.
  </Accordion>

  <Accordion title="Why did GET training return 404?">
    No training run has been started for that import. POST first, then poll.
  </Accordion>
</AccordionGroup>

## Next

<CardGroup cols={2}>
  <Card title="Import a Policy" icon="file-import" href="/guides/import-a-policy">
    Get an activated `IMPORT_ID` before you train.
  </Card>

  <Card title="Train Policy Adapter" icon="play" href="/api-reference/training-studio/train-policy">
    POST `/api/policies/import/{import_id}/train`
  </Card>
</CardGroup>
