> ## 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.

# Conditioning your own agent

> Get a ready-to-inject system prompt that makes your own LLM reason and sound like the subject, then cache it until it expires.

Most memory APIs hand you facts and leave the prompt engineering to you. `POST /v1/condition/session` hands you the prompt. It composes the subject's identity, their current mode of thinking and a digest of recent organization activity into one system prompt you paste into Claude, GPT, or a local model. One identity, any LLM.

## Before you start

<Columns cols={3}>
  <Card title="A key with read" icon="key" href="/scopes">
    Conditioning is a read. Every key can do it.
  </Card>

  <Card title="Enough history" icon="clock" href="/concepts/memory-lifecycle">
    A pack composed over an empty graph is thin. Ingest first.
  </Card>

  <Card title="An LLM you control" icon="bot" href="#injecting-the-pack">
    You inject the prompt yourself. Exo does not call your model.
  </Card>
</Columns>

## The shortest call that works

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  curl -X POST https://api.get-exo.com/v1/condition/session \
    -H "X-Exo-API-Key: $EXO_KEY" \
    -H "Content-Type: application/json" \
    -d '{
          "userId": "4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93",
          "orgId": "0d5b7c92-3a41-4e88-9b02-6f7c1d4a5e30",
          "cwd": "/srv/checkout-service",
          "gitBranch": "feat/pricing-v2",
          "recentOrgActivityHours": 24
        }'
  ```

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

  r = httpx.post(
      "https://api.get-exo.com/v1/condition/session",
      headers={"X-Exo-API-Key": EXO_KEY},
      json={
          "userId": "4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93",
          "orgId": "0d5b7c92-3a41-4e88-9b02-6f7c1d4a5e30",
          "cwd": "/srv/checkout-service",
          "gitBranch": "feat/pricing-v2",
          "recentOrgActivityHours": 24,
      },
  )
  pack = r.json()
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  const r = await fetch("https://api.get-exo.com/v1/condition/session", {
    method: "POST",
    headers: {
      "X-Exo-API-Key": process.env.EXO_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      userId: "4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93",
      orgId: "0d5b7c92-3a41-4e88-9b02-6f7c1d4a5e30",
      cwd: "/srv/checkout-service",
      gitBranch: "feat/pricing-v2",
      recentOrgActivityHours: 24,
    }),
  });
  const pack = await r.json();
  ```
</CodeGroup>

```json 200 OK theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "systemPrompt": "You are assisting someone who reasons bottom-up from data and prefers direct answers. They are currently in a rapid implementation mode: they want the change made, not the options enumerated. Their prior positions on pricing favour predictability for the buyer over revenue capture.",
  "userVoiceHint": "short sentences, technical, no filler, no hedging",
  "orgRecentContext": [
    { "title": "Pricing working session", "summary": "Seat plus usage won on predictability." }
  ],
  "expiresAt": "2026-07-30T21:00:00Z",
  "usage": { "readUnits": 1, "llmInputTokens": null, "llmOutputTokens": null }
}
```

<Note>
  `cwd`, `userId` and `orgId` are required by the schema because this route began as a coding-agent session hook. **They do not select the subject.** The effective subject comes from your API key, or from the `X-Exo-Subject` header when you send one. Send the body fields, and send the header when you mean a different person.
</Note>

## Reading the pack

| Field              | Type           | What it is                                                    |
| ------------------ | -------------- | ------------------------------------------------------------- |
| `systemPrompt`     | string         | The composed prompt. Inject it verbatim as the system message |
| `userVoiceHint`    | string or null | How this person actually writes, measured from their own text |
| `orgRecentContext` | array          | Recent organization activity worth folding into context       |
| `expiresAt`        | date-time      | How long the pack stays fresh enough to cache                 |
| `usage.readUnits`  | integer        | One unit per compose call                                     |

**`userVoiceHint` is null until there is enough of the subject's own writing to measure.** Exo withholds the voice profile rather than guessing at it, so treat null as "no voice guidance yet", not as an error. See [ConditioningPack and VoiceProfile](/concepts/conditioning-pack).

`usage.llmInputTokens` and `llmOutputTokens` are null because composing the pack is a set of database reads with no model call. They exist so that a future builder version that does spend tokens can report them in the same place.

## Injecting the pack

The pack is designed to be dropped in as the system message, ahead of your own instructions.

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  PACK=$(curl -s -X POST https://api.get-exo.com/v1/condition/session \
    -H "X-Exo-API-Key: $EXO_KEY" -H "Content-Type: application/json" \
    -d '{"userId":"4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93","orgId":"0d5b7c92-3a41-4e88-9b02-6f7c1d4a5e30","cwd":"/srv/checkout-service"}')

  SYSTEM=$(echo "$PACK" | jq -r '.systemPrompt')
  VOICE=$(echo "$PACK" | jq -r '.userVoiceHint // empty')

  curl https://api.anthropic.com/v1/messages \
    -H "x-api-key: $ANTHROPIC_API_KEY" \
    -H "anthropic-version: 2023-06-01" \
    -H "content-type: application/json" \
    -d "$(jq -n --arg s "$SYSTEM${VOICE:+

  Write in this voice: $VOICE}" \
          '{model:"claude-sonnet-4-6", max_tokens:1024, system:$s,
            messages:[{role:"user", content:"Draft the pricing change announcement."}]}')"
  ```

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

  pack = httpx.post(
      "https://api.get-exo.com/v1/condition/session",
      headers={"X-Exo-API-Key": EXO_KEY},
      json={"userId": "4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93", "orgId": "0d5b7c92-3a41-4e88-9b02-6f7c1d4a5e30", "cwd": "/srv/checkout-service"},
  ).json()

  system = pack["systemPrompt"]
  if pack.get("userVoiceHint"):
      system += f"\n\nWrite in this voice: {pack['userVoiceHint']}"

  msg = anthropic.Anthropic().messages.create(
      model="claude-sonnet-4-6",
      max_tokens=1024,
      system=system,
      messages=[{"role": "user", "content": "Draft the pricing change announcement."}],
  )
  print(msg.content[0].text)
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  import Anthropic from "@anthropic-ai/sdk";

  const pack = await (
    await fetch("https://api.get-exo.com/v1/condition/session", {
      method: "POST",
      headers: {
        "X-Exo-API-Key": process.env.EXO_KEY!,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        userId: "4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93",
        orgId: "0d5b7c92-3a41-4e88-9b02-6f7c1d4a5e30",
        cwd: "/srv/checkout-service",
      }),
    })
  ).json();

  let system: string = pack.systemPrompt;
  if (pack.userVoiceHint) system += `\n\nWrite in this voice: ${pack.userVoiceHint}`;

  const msg = await new Anthropic().messages.create({
    model: "claude-sonnet-4-6",
    max_tokens: 1024,
    system,
    messages: [{ role: "user", content: "Draft the pricing change announcement." }],
  });
  ```
</CodeGroup>

The same pack works against any provider. Nothing in it is Claude-specific.

## Caching

Respect `expiresAt`. Composing on every message wastes read units and gains nothing, because the underlying identity moves on the scale of sessions, not turns.

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  # Store the pack next to the session and refetch only past expiresAt.
  EXPIRES=$(echo "$PACK" | jq -r '.expiresAt')
  NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
  [ "$NOW" \> "$EXPIRES" ] && echo "refetch the pack"
  ```

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

  _cache: dict[str, dict] = {}

  def get_pack(subject: str) -> dict:
      hit = _cache.get(subject)
      if hit and datetime.fromisoformat(hit["expiresAt"]) > datetime.now(timezone.utc):
          return hit
      pack = httpx.post(
          "https://api.get-exo.com/v1/condition/session",
          headers={"X-Exo-API-Key": EXO_KEY, "X-Exo-Subject": subject},
          json={"userId": subject, "orgId": ORG_ID, "cwd": "/srv/checkout-service"},
      ).json()
      _cache[subject] = pack
      return pack
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  const cache = new Map<string, { pack: any; expires: number }>();

  async function getPack(subject: string) {
    const hit = cache.get(subject);
    if (hit && hit.expires > Date.now()) return hit.pack;

    const pack = await (
      await fetch("https://api.get-exo.com/v1/condition/session", {
        method: "POST",
        headers: {
          "X-Exo-API-Key": process.env.EXO_KEY!,
          "X-Exo-Subject": subject,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ userId: subject, orgId: ORG_ID, cwd: "/srv/checkout-service" }),
      })
    ).json();

    cache.set(subject, { pack, expires: Date.parse(pack.expiresAt) });
    return pack;
  }
  ```
</CodeGroup>

Invalidate early when you receive a `basin.shifted` event, which is Exo telling you the subject's mode of thinking moved. See [Reacting to change](/guides/reacting-to-change).

## Agents that build their own prompts

If your agent assembles context itself and does not want a whole system prompt, use `POST /v1/condition/scaffold`. It wraps one prompt in the subject's cognitive scaffold and tells you which knowledge-graph nodes shaped it.

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl -X POST https://api.get-exo.com/v1/condition/scaffold \
  -H "X-Exo-API-Key: $EXO_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "userId": "4a1e88f0-9c2d-4f31-b7a0-5e6c1d820b93",
        "prompt": "Should we move billing to seat plus usage?",
        "recentSessionSummary": "Reviewed churn by plan tier.",
        "mode": "default"
      }'
```

```json 200 OK theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "scaffold": "Context that bears on this prompt: the pricing working session concluded seat plus usage; an earlier position favoured usage-only.\n\nShould we move billing to seat plus usage?",
  "focalNodes": [],
  "usage": { "readUnits": 1, "llmInputTokens": null, "llmOutputTokens": null }
}
```

`prompt` accepts up to 100,000 characters.

<Warning>
  **`focalNodes` is always an empty array in v1.** The scaffold is composed from the subject's identity rather than from a retrieval pass, so there are no focal nodes to report. Do not build a citation UI on this field. If you need the nodes behind an answer, read `cognition.focalNodeIds` from [`POST /v1/retrieve`](/api-reference/retrieve/post-retrieve), which is populated on the brain path. Treat a future non-empty list here as an additive change.
</Warning>

| Use                                             | Route                         |
| ----------------------------------------------- | ----------------------------- |
| Start a session as this person                  | `POST /v1/condition/session`  |
| Enrich one prompt, keep your own system message | `POST /v1/condition/scaffold` |

## Conditioning on behalf of a subject

Add `X-Exo-Subject`, exactly as on every other route.

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl -X POST https://api.get-exo.com/v1/condition/session \
  -H "X-Exo-API-Key: $EXO_KEY" \
  -H "X-Exo-Subject: customer_6412" \
  -H "Content-Type: application/json" \
  -d '{"userId":"customer_6412","orgId":"0d5b7c92-3a41-4e88-9b02-6f7c1d4a5e30","cwd":"/app"}'
```

This is the call that gives every end user of your product their own conditioned agent.

## When it goes wrong

| What you see              | Why                                                                                    | What to do                                                                                                                  |
| ------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `422 validation_error`    | `cwd`, `userId` or `orgId` is missing, or `recentOrgActivityHours` is outside 1 to 168 | All three fields are required even though they do not select the subject. See [validation\_error](/errors/validation_error) |
| `userVoiceHint` is `null` | Not enough of the subject's own writing to measure a voice                             | Expected on a young graph. Ingest more of what they wrote, not what was written to them                                     |
| The prompt is generic     | The graph is thin, or the background cycle has not run yet                             | Check `GET /v1/recall/identity` for a manifold. See [How a memory is made](/concepts/memory-lifecycle)                      |
| `403 permission_denied`   | `X-Exo-Subject` names an unprovisioned subject                                         | `PUT /v1/subjects/{id}` first. See [permission\_denied](/errors/permission_denied)                                          |
| `503 service_degraded`    | A dependency is briefly unavailable                                                    | Retry with backoff, or serve your cached pack past `expiresAt` rather than failing the session                              |

## Next

<Columns cols={2}>
  <Card title="ConditioningPack and VoiceProfile" icon="user-round" href="/concepts/conditioning-pack">
    Every field, and why the voice profile is withheld rather than guessed.
  </Card>

  <Card title="Reacting to change" icon="radio" href="/guides/reacting-to-change">
    Invalidate a cached pack the moment the subject's thinking shifts.
  </Card>
</Columns>
