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

# Training and serving an ExoBrain

> Train the per-organization model that turns retrieval from search into reasoning, then read the glass-box signals it produces.

An **ExoBrain** is a small neural model trained on one organization's knowledge graph. It is what makes `POST /v1/retrieve` return a `cognition` block instead of a ranked list. Without one, retrieval still works and says so honestly. With one, you get focal nodes, gate confidence and a fast-versus-slow reasoning tier.

## Before you start

<Columns cols={3}>
  <Card title="An admin key" icon="shield" href="/scopes">
    `POST /v1/brain/train` requires the `admin` scope. Reads do not.
  </Card>

  <Card title="A graph with content" icon="database" href="/guides/ingesting">
    A brain trains on the graph. Too few nodes and the train is refused.
  </Card>

  <Card title="Patience for the job" icon="clock" href="#training">
    Training runs on GPU hardware as a queued job, not inline.
  </Card>
</Columns>

## Check what you have now

Start here, because the answer is often "nothing yet, and that is fine".

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  curl https://api.get-exo.com/v1/brain/info \
    -H "X-Exo-API-Key: $EXO_KEY"
  ```

  ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  import httpx

  info = httpx.get(
      "https://api.get-exo.com/v1/brain/info",
      headers={"X-Exo-API-Key": EXO_KEY},
  ).json()
  print(info["training"]["status"])
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  const info = await (
    await fetch("https://api.get-exo.com/v1/brain/info", {
      headers: { "X-Exo-API-Key": process.env.EXO_KEY! },
    })
  ).json();
  console.log(info.training.status);
  ```
</CodeGroup>

`GET /v1/brain/info` reports which artifact is loaded for your organization, how big it is in model parameters and in graph nodes and edges, which backend is serving it, and a `training` block describing the most recent or in-flight train. When the organization has never trained, it **degrades to a `training` status of `none` rather than failing**, so this call is always safe to make.

<Note>
  The response shape for this route is not pinned in the OpenAPI contract, so treat the field names above as the documented contents rather than a fixed schema, and read defensively. Everything else on this page is schema-pinned.
</Note>

## Training

<Steps>
  <Step title="Enqueue the train" icon="cpu">
    <CodeGroup>
      ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      curl -X POST https://api.get-exo.com/v1/brain/train \
        -H "X-Exo-API-Key: $EXO_ADMIN_KEY" \
        -H "Content-Type: application/json" \
        -d '{"reason": "nightly refresh", "epochsScale": 1.0}'
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      r = httpx.post(
          "https://api.get-exo.com/v1/brain/train",
          headers={"X-Exo-API-Key": EXO_ADMIN_KEY},
          json={"reason": "nightly refresh", "epochsScale": 1.0},
      )
      job = r.json()
      ```

      ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      const r = await fetch("https://api.get-exo.com/v1/brain/train", {
        method: "POST",
        headers: {
          "X-Exo-API-Key": process.env.EXO_ADMIN_KEY!,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ reason: "nightly refresh", epochsScale: 1.0 }),
      });
      const job = await r.json();
      ```
    </CodeGroup>

    ```json 202 Accepted theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    {
      "jobId": "7c04b1e8-3a55-4f19-90d2-b1e6a2c73f40",
      "orgId": "0d5b7c92-3a41-4e88-9b02-6f7c1d4a5e30",
      "status": "pending"
    }
    ```

    Both body fields are optional. `reason` defaults to `"manual"` and is there so your audit log says why. `epochsScale` defaults to `1.0`; raise it for a longer train, lower it for a cheaper one.
  </Step>

  <Step title="Watch it" icon="loader">
    Poll `GET /v1/brain/info` and read the `training` block. Only one train may be in flight per organization, so a second request while one is queued or running returns `409`. That is a guard, not a failure: read it as "already training".

    ```python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    import time

    while True:
        t = httpx.get(
            "https://api.get-exo.com/v1/brain/info",
            headers={"X-Exo-API-Key": EXO_KEY},
        ).json()["training"]
        if t["status"] not in ("pending", "running"):
            break
        time.sleep(30)
    ```

    You can also subscribe to `brain.trained`, an organization-scoped event that reaches every listener. See [Reacting to change](/guides/reacting-to-change).
  </Step>

  <Step title="Confirm it is serving" icon="check">
    A successful train does not silently change your reads. Confirm by looking at `path` on a retrieval:

    ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    curl -s -X POST https://api.get-exo.com/v1/retrieve \
      -H "X-Exo-API-Key: $EXO_KEY" -H "Content-Type: application/json" \
      -d '{"query": "pricing"}' | jq '{path, degraded, degradationReason}'
    ```

    ```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    { "path": "brain", "degraded": false, "degradationReason": null }
    ```

    Before the train this reads `{"path": "hybrid", "degraded": true, "degradationReason": "no_brain"}`. The flip is the proof.
  </Step>
</Steps>

<Note>
  **A failed train leaves the previous brain serving.** The new artifact is published only after it passes a numerical parity gate, so a bad train degrades nothing. There is no window where your organization is running on a half-trained model.
</Note>

## Querying the brain directly

`POST /v1/brain/query` runs the model and hands back its internals rather than an answer. This is what you build a "what did it attend to" view on.

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  curl -X POST https://api.get-exo.com/v1/brain/query \
    -H "X-Exo-API-Key: $EXO_KEY" \
    -H "Content-Type: application/json" \
    -d '{"question": "Where did we land on pricing?", "seed_k": 200, "focal_display": 5}'
  ```

  ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  r = httpx.post(
      "https://api.get-exo.com/v1/brain/query",
      headers={"X-Exo-API-Key": EXO_KEY},
      json={"question": "Where did we land on pricing?", "seed_k": 200, "focal_display": 5},
  )
  out = r.json()
  for node in out["focal"]:
      print(node["node_id"], node["cosine_in_seed"], node["snippet"][:80])
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  const out = await (
    await fetch("https://api.get-exo.com/v1/brain/query", {
      method: "POST",
      headers: {
        "X-Exo-API-Key": process.env.EXO_KEY!,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        question: "Where did we land on pricing?",
        seed_k: 200,
        focal_display: 5,
      }),
    })
  ).json();
  out.focal.forEach((n: any) => console.log(n.node_id, n.cosine_in_seed));
  ```
</CodeGroup>

```json 200 OK theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "question": "Where did we land on pricing?",
  "tenant_id": "0d5b7c92-3a41-4e88-9b02-6f7c1d4a5e30",
  "alias": "base",
  "focal": [
    {
      "node_id": "ingest:4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93:7d3c9b1e04a2",
      "snippet": "Seat plus usage won on predictability.",
      "cosine_in_seed": true
    }
  ],
  "cosine_top": [],
  "confidence": 0.83,
  "tier": "growing",
  "use_system2": false,
  "drift_rate": 0.02,
  "contradictions": [],
  "negative_edges_in_subgraph": 4,
  "system_used": "system1",
  "latency_ms": 118.4,
  "subgraph": { "nodes": 1284, "edges": 5106 }
}
```

<Note>
  `tier` and `system_used` are two different measurements and are easy to confuse. **`tier`** describes how much graph the brain has to work with, and its values are `seed`, `sprout`, `growing`, `mature` and `deep`, assigned by node count. **`system_used`** describes how it thought about this one query, and its values are `system1` and `system2`. A `tier` of `"system1"` is not a value the API produces.
</Note>

<Warning>
  **The brain routes use `snake_case`**, unlike the rest of the API, which is camelCase. `seed_k`, `focal_display`, `drift_rate`, `use_system2`, `latency_ms`, `model_params` and `cosine_in_seed` are all correct as written. Do not camelCase them.
</Warning>

The field worth understanding is `cosine_in_seed`. It tells you whether a focal node was also in the plain cosine top-K head. A focal node with `cosine_in_seed: false` is one the brain reached by propagating through the graph, not by similarity. That gap is the difference between search and reasoning, and it is the interesting thing to visualize.

`seed_k` controls the cosine seed set size, from 10 to 2000, and drives the reasoning tier. `focal_display` caps how many focal nodes come back, from 1 to 20.

<Note>
  `POST /v1/brain/query` and `POST /v1/brain/answer` read **the API key owner's own content**. The `X-Exo-Subject` selector is not accepted on either route. To reason over a subject's data, use `POST /v1/retrieve` with the header instead.
</Note>

## Generating a conditioned answer

`POST /v1/brain/answer` produces text. `question` and `arm` are both required; `k` defaults to 8 retrieval chunks.

With `arm` set to `"brain"` the forward pass runs first, and `tercile_boundaries` decides what happens next:

| `tercile_boundaries` | What you get                                                 |
| -------------------- | ------------------------------------------------------------ |
| Omitted              | Raw signals, generation skipped, `answer` is an empty string |
| Supplied             | Signals rendered into the prompt, then the answer generated  |

That two-step design exists so you can collect signals across a batch of questions, compute your own cut points from the distribution, then generate with boundaries that mean something for your data rather than arbitrary thresholds.

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
# Pass 1: collect signals, no generation.
curl -X POST https://api.get-exo.com/v1/brain/answer \
  -H "X-Exo-API-Key: $EXO_KEY" -H "Content-Type: application/json" \
  -d '{"question": "Where did we land on pricing?", "arm": "brain", "k": 8}'

# Pass 2: generate, with boundaries derived from pass 1.
curl -X POST https://api.get-exo.com/v1/brain/answer \
  -H "X-Exo-API-Key: $EXO_KEY" -H "Content-Type: application/json" \
  -d '{
        "question": "Where did we land on pricing?",
        "arm": "brain",
        "k": 8,
        "tercile_boundaries": {
          "confidence": [0.42, 0.71],
          "drift_rate": [0.03, 0.11]
        }
      }'
```

`confidence` and `drift_rate` each take two cut points, `[lowMax, midMax]`, splitting the range into low, middle and high bands. The response carries `answer`, `prompt_used`, `retrieval_chunk_ids` and a `brain_signals` block with `confidence`, `drift_rate`, `use_system2`, `tier`, `contradictions` and `appendix_lines`.

## Keeping a brain fresh

A brain trains on a snapshot. As the graph grows, the snapshot ages, and retrieval tells you:

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{ "path": "brain", "degraded": true, "degradationReason": "brain_stale" }
```

`brain_stale` means the brain is still answering, and answering well, but the graph has moved on. It is a signal to schedule a retrain, not an incident. Two other reasons appear on the `hybrid` path: `no_brain` when none has been trained, and `cold_start` when there is not yet enough content to do better.

Retrain when:

* `degradationReason` has been `brain_stale` for a while.
* You imported a large corpus.
* You erased a subject. `DELETE /v1/subjects/{id}` marks the brain stale rather than deleting it, because it was trained on every subject. Retraining is what removes that person's influence from the model. See [Forgetting and portability](/guides/forgetting).

## When it goes wrong

| What you see                                   | Why                                                        | What to do                                                                                                           |
| ---------------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `403 permission_denied` on train               | The key lacks `admin`                                      | Mint an admin key. Reads need no special scope. See [permission\_denied](/errors/permission_denied)                  |
| `409` on train                                 | A train is already queued or running for this organization | Wait for it. One in flight per organization is the rule                                                              |
| `404` on `/v1/brain/query`                     | No brain artifact exists yet                               | Train first, or use `POST /v1/retrieve`, which falls back gracefully                                                 |
| `409` on `/v1/brain/query` right after a train | A newly published artifact is still being picked up        | Retry shortly. The serve path reloads when it notices the new artifact                                               |
| Train succeeded, `path` is still `hybrid`      | The serve path has not reloaded yet                        | Give it a moment and re-check. If it persists, read the `training` block on `GET /v1/brain/info` for the real status |
| Train fails repeatedly                         | Too few nodes, or the artifact failed its parity gate      | The previous brain keeps serving. Ingest more content before retrying                                                |
| `503 service_degraded`                         | A dependency is briefly unavailable                        | Retry with backoff. See [service\_degraded](/errors/service_degraded)                                                |

## Next

<Columns cols={2}>
  <Card title="ExoBrain" icon="cpu" href="/concepts/exobrain">
    What the per-organization model is, when it serves, and when it is stale.
  </Card>

  <Card title="Brain path and hybrid path" icon="git-fork" href="/concepts/retrieval-paths">
    What `path`, `degraded` and each `degradationReason` actually mean.
  </Card>
</Columns>
