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

# Migrating to Extractions

> Migrate off custom_extractions, agent_extraction, and flat extracted_data keys before legacy post-call extraction features are removed on 18 September 2026.

<Warning>
  **Legacy features off — 18 September 2026.** After this date, `custom_extractions` and `agent_extraction` return `null`, and flat keys stop appearing in `extracted_data`. All results consolidate into `extracted_data`, nested `category → extraction name → result object`. Each value changes from a scalar to an object. This is a breaking change for any webhook, API, or CSV consumer.
</Warning>

Both shapes are populated today. Deploy the new read path, diff it against the old one on live calls, then disable the legacy features.

## Am I affected?

Check a recent webhook payload or `GET /executions` response:

| Field present with a value      | Toggle in the dashboard | What to do             |
| ------------------------------- | ----------------------- | ---------------------- |
| `extracted_data` with flat keys | Extraction              | Rebuild as extractions |
| `custom_extractions`            | Custom Analytics        | Rebuild, one per item  |
| `agent_extraction`              | Agent Extraction        | Discontinued           |
| `summary`                       | Call Summary            | No change required     |

<Frame caption="The agent Extractions tab showing the Extraction toggle, the Agent Extraction toggle marked Internal, and the Custom Analytics section, under a heading badged to be deprecated.">
  <img src="https://mintcdn.com/bolna-54a2d4fe/N9_gVvV4A3n6PkOI/images/old_extractions.png?fit=max&auto=format&n=N9_gVvV4A3n6PkOI&q=85&s=2d819b5120fec6aacedc3dbeb512b1fa" alt="Legacy Extraction, Agent Extraction, and Custom Analytics toggles in the agent Extractions tab" width="1958" height="804" data-path="images/old_extractions.png" />
</Frame>

Note the naming overlap: the block being retired is also labelled "Extractions." The **to be deprecated** badge is what tells them apart.

<Note>
  **Already migrated?** Only nested keys means you are already on Extractions and no action is needed. Both shapes can appear together during the transition, with flat legacy keys sitting alongside nested categories. Check for flat keys specifically, rather than assuming you are done because nested ones are present.
</Note>

## Migration by feature

Four features, four different paths. Open the one whose field appears in your payload.

<CardGroup cols={2}>
  <Card title="Extraction" icon="list-check" href="/docs/migrating-to-extractions/extraction">
    `extracted_data` · flat keys → **Rebuild as extractions.** One extraction per field in your prose box. Keys stay stable if you reuse your current names.
  </Card>

  <Card title="Custom Analytics" icon="sliders" href="/docs/migrating-to-extractions/custom-analytics">
    `custom_extractions` → **Rebuild, one per item.** Types map onto answer types. Numbers become strings, and min/max clamping has no equivalent — use `validation.is_valid` instead.
  </Card>

  <Card title="Agent Extraction" icon="robot" href="/docs/migrating-to-extractions/agent-extraction">
    `agent_extraction` → **Discontinued.** No replacement template and no migration on your behalf. Identify the fields from a payload and author your own.
  </Card>

  <Card title="Call Summary" icon="file-lines" href="/docs/migrating-to-extractions/call-summary">
    `summary` → **No change required.** Stays a flat top-level key, now sourced from the Call Summary extraction. Keep reading it as you do today.
  </Card>
</CardGroup>

## What the payload looks like

Category is a grouping label you choose. Name is the key you choose.

<CodeGroup>
  ```json Before theme={"system"}
  {
    "summary": "The agent confirmed…",
    "extracted_data": {
      "call_reason": "Billing dispute",
      "preferred_day": "Thursday"
    },
    "custom_extractions": {
      "loan_amount": 250000.0,
      "eligibility_key": "Eligible"
    },
    "agent_extraction": {
      "asst_0DxYqjpXy2EObi8c": {
        "Final Rating": "Strong Fit"
      }
    }
  }
  ```

  ```json After migration theme={"system"}
  {
    "summary": "The agent confirmed…",
    "custom_extractions": null,
    "agent_extraction": null,
    "extracted_data": {
      "Call Details": {
        "Call Reason": {
          "subjective": null,
          "objective": "Billing_Dispute",
          "confidence": 0.88,
          "confidence_label": "High",
          "reasoning_subjective": null,
          "reasoning_objective": "Charged twice…",
          "validation": null
        }
      }
    }
  }
  ```
</CodeGroup>

### Result object fields

| Field                  | Type             | Description                                                                                              |
| ---------------------- | ---------------- | -------------------------------------------------------------------------------------------------------- |
| `subjective`           | `string \| null` | Free-text answer. `null` if free text is disabled.                                                       |
| `objective`            | `string \| null` | Selected pre-defined value, exactly as configured. `null` if pre-defined answers are disabled.           |
| `confidence`           | `float`          | 0.0–1.0, 3 decimal places.                                                                               |
| `confidence_label`     | `string`         | `"High"` ≥ 0.8, `"Medium"` ≥ 0.5, `"Low"` below that.                                                    |
| `reasoning_subjective` | `string \| null` | Why the LLM gave that free-text answer.                                                                  |
| `reasoning_objective`  | `string \| null` | Why the LLM selected that option.                                                                        |
| `validation`           | `object \| null` | `null` for plain text. Otherwise `{"is_valid": bool, "expected_type": "…"}`, plus `"pattern"` for regex. |

**Which field to read:**

| Configuration    | Read                                   |
| ---------------- | -------------------------------------- |
| Pre-defined only | `objective`                            |
| Free text only   | `subjective`                           |
| Both             | `objective`, fall back to `subjective` |

## Breaking changes

* **Values are two levels deeper.** `extracted_data.call_reason` becomes `extracted_data["Call Details"]["Call Reason"]`.
* **Values are objects, not scalars.** Select `objective` or `subjective` from the result.
* **`subjective` and `objective` are always JSON strings.** A numeric extraction returns `"250000"`, not `250000`. Custom Analytics returned a real number, so parse it yourself. Min/max clamping has no equivalent; use `validation.is_valid` to confirm the value parses.
* **`custom_extractions` and `agent_extraction` return `null`** once the feature is removed from the agent, or on 18 September 2026. The keys stay in the payload.
* **`summary` stays a flat top-level key.** Webhook payloads keep `summary` as a string at the top level, now populated by the Call Summary extraction instead of the legacy summarisation task. If you read top-level `summary` today, no change is required.
* **`objective` cannot drift.** Pre-defined answers are constrained by the request, so the value matches your configured option character for character. Switch fuzzy matching to exact comparison.
* **New capabilities: per-answer confidence, LLM reasoning, and format validation.** Route `confidence_label: "Low"` to human review.

### Expected formats

| Expected format                            | Validation                                                          |
| ------------------------------------------ | ------------------------------------------------------------------- |
| `text` (default)                           | `null`                                                              |
| `numeric`, `boolean`, `email`, `timestamp` | `{"is_valid": true\|false, "expected_type": "<format>"}`            |
| `regex`                                    | `{"is_valid": …, "expected_type": "regex", "pattern": "<pattern>"}` |

A failed check never loses the value. `is_valid: false` is set and `subjective` holds the LLM's response unchanged.

## Calls with no user speech

This ships on 18 September 2026, alongside the deprecation. It applies to every agent, including ones already using Extractions.

When a call contains no user turns (voicemail, an immediate hangup, dead air), the LLM has nothing to work from. Today it can still invent a plausible answer. From 18 September 2026 it will instead return a fixed sentinel across every extraction on the agent.

```json No user turns detected, from 18 September 2026 theme={"system"}
"extracted_data": {
  "Sales": {
    "Interest Level": {
      "subjective": "No User Turn Detected",
      "objective": "No User Turn Detected",
      "confidence": 1.0,
      "confidence_label": "High",
      "reasoning_subjective": "No User Turn Detected",
      "reasoning_objective": "No User Turn Detected",
      "validation": null
    }
  }
}
```

`No User Turn Detected` will appear in `subjective`, `objective`, `reasoning_subjective` and `reasoning_objective`, in every extraction across every category.

<Warning>
  * The top-level `summary` key will return it too. Anything writing `summary` into a CRM note, ticket field, or report will write that string. Check for it before persisting.
  * `objective` will return the sentinel even though it is not one of your configured options. Exact-match comparisons will not hit any branch, so handle the sentinel before comparing.
  * `confidence` will be `1.0` and `confidence_label` `"High"`. The score reflects certainty that no user spoke, not certainty about an answer. Filtering on confidence alone will not exclude these calls.
  * Typed extractions will return the sentinel as a string. A numeric extraction returns `"No User Turn Detected"`, so guard your parsing.
</Warning>

Check for the sentinel before reading any extraction value, and route these calls to your no-contact path rather than treating them as answered.

## CSV exports

Seven columns per extraction instead of one, following `extracted_data_<Category>_<Name>_<field>`.

| Today                        | After migration                                                                                                                                                              |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `extracted_data_call_reason` | `extracted_data_Call Details_Call Reason_objective`, `…_subjective`, `…_confidence`, `…_confidence_label`, `…_reasoning_objective`, `…_reasoning_subjective`, `…_validation` |
| `summary`                    | `summary`, unchanged                                                                                                                                                         |

Update any loader or formula referencing columns by name.

## Configuring through the API

<Note>
  Extractions are managed under `/dispositions`. "Disposition" is the API's name for a single extraction. The `/extractions` path is an internal template registry, not this endpoint.
</Note>

| Method   | Endpoint                                 | Description                                    |
| -------- | ---------------------------------------- | ---------------------------------------------- |
| `GET`    | `/dispositions/?agent_id={agent_id}`     | List an agent's extractions plus platform ones |
| `GET`    | `/dispositions/models`                   | Allowed models                                 |
| `GET`    | `/dispositions/library`                  | Reusable library items                         |
| `POST`   | `/dispositions/`                         | Create one and attach it to an agent           |
| `POST`   | `/dispositions/bulk`                     | Create several at once                         |
| `POST`   | `/dispositions/copy`                     | Copy one onto another agent                    |
| `PUT`    | `/dispositions/{id}`                     | Update                                         |
| `DELETE` | `/dispositions/{id}`                     | Remove                                         |
| `POST`   | `/v2/agent/{agent_id}/dispositions/test` | Dry-run every extraction on a transcript       |

### Creating one

```json POST /dispositions/ theme={"system"}
{
  "agent_id": "3f9a1c22-7b40-4e51-9c8d-6a2f0b13d7e4",
  "name": "Call Reason",
  "category": "Call Details",
  "question": "Why did the customer call? Choose the closest reason and ignore small talk.",
  "is_objective": true,
  "objective_options": [
    { "value": "Billing_Dispute", "condition": "The customer questions or disputes a charge." },
    { "value": "Appointment",     "condition": "The customer books, moves or cancels an appointment." },
    { "value": "NA",              "condition": "The reason for the call never became clear." }
  ],
  "is_subjective": false,
  "subjective_type": "text"
}
```

* `name` is the result key. `category` is the key above it.
* `question` is the LLM prompt. State what to look at, what to ignore, and what each option means.
* Always include a fallback option such as `NA` so the model is never forced to pick a real answer for a question the call never reached.
* Set `is_subjective: true` for free text, `is_objective: true` for pre-defined, or both.
* `subjective_type` accepts `text`, `numeric`, `boolean`, `email`, `timestamp`, `regex`. `regex` also requires `subjective_type_config: {"pattern": "…"}`.
* Extractions in one category are answered in a single LLM call, so grouping related fields keeps their answers consistent with each other.

### Verifying a migration

```json POST /v2/agent/{agent_id}/dispositions/test theme={"system"}
{ "transcript": "assistant: …\nuser: …", "call_date": "2026-08-31T10:00:00" }
```

Returns `{"extracted_data": {…}}` in exactly the shape a live call produces. Paste a real past transcript and diff against what your old configuration returned for that call. No call is spent.

## Running both side by side

`extracted_data` is a shared field. While a legacy extraction task and Extractions are both enabled, one object holds both shapes, with flat legacy keys alongside nested categories:

```json During the transition theme={"system"}
{
  "extracted_data": {
    "call_reason": "Billing dispute",
    "Call Details": {
      "Call Reason": { "objective": "Billing_Dispute", … }
    }
  }
}
```

Check which shape you have rather than assuming. A nested category is an object whose own values are objects containing `subjective` and `objective`. Branching on the nested shape stays correct during and after the transition.

<Warning>
  **Namespace collision.** Do not give a category the same name as a legacy extraction key while both run. They share one namespace and will collide.
</Warning>

## Timeline

<Steps>
  <Step title="Now">
    Old and new run side by side. Newly created agents get Extractions only.
  </Step>

  <Step title="18 September 2026">
    `custom_extractions` and `agent_extraction` return `null`. Flat keys stop appearing in `extracted_data`. `summary` continues, sourced from the Call Summary extraction. Unmigrated configurations return no results.
  </Step>
</Steps>

<Warning>
  No extension and no dual-write period past 18 September. Migration is self-serve, so we do not convert or map configurations on your behalf. Raise blockers this week, not in the final week.
</Warning>

## FAQ

<AccordionGroup>
  <Accordion title="Is historical data converted?">
    No. Completed calls keep the results and shape they were stored with, readable indefinitely. Only calls placed after you migrate use the new shape. Code that reads historical calls must keep handling the old shape permanently, not just until the cutoff.
  </Accordion>

  <Accordion title="Can I run both during the transition?">
    Yes, and it is the recommended path. Change the read path first, then disable the legacy features once you have compared the two on live calls. See [Running both side by side](#running-both-side-by-side).
  </Accordion>

  <Accordion title="Will results be worded differently?">
    Free-text answers may be phrased differently for an identical call, because the prompt structure differs. Pre-defined answers are stable, since they can only be one of your configured values. Diff side by side before disabling anything.
  </Accordion>

  <Accordion title="Can I keep my field names?">
    Yes. Set each extraction's name to your current key. The name is unchanged; the path is one level deeper.
  </Accordion>

  <Accordion title="Will you migrate my configuration for me?">
    No. Recreating extractions is self-serve, through the dashboard or the `/dispositions` API. This includes Agent Extraction, which is discontinued rather than replaced.
  </Accordion>

  <Accordion title="What happens on 18 September if I have not migrated?">
    Agents keep taking calls. Only structured results stop: legacy fields return `null`, and no extractions run unless configured. Transcripts, recordings and call summaries are unaffected.
  </Accordion>

  <Accordion title="My payload does not match these pages.">
    Email [support@bolna.ai](mailto:support@bolna.ai) with the `execution_id` of the call, the field you were reading, and the value you expected. The `execution_id` is the id in the webhook payload and in `GET /executions` responses. One id is much quicker for us to work with than a description of the problem — we can pull that exact execution and see which extraction path produced the value.
  </Accordion>
</AccordionGroup>

Email [support@bolna.ai](mailto:support@bolna.ai) to remove a legacy summarisation task, or with an `execution_id` for a payload that does not match this guide.

## Next Steps

<CardGroup cols={3}>
  <Card title="Using Extractions" icon="list-check" href="/docs/prompting/using-extractions">
    Configure extractions in the dashboard
  </Card>

  <Card title="Dispositions API" icon="code" href="/docs/api-reference/dispositions/overview">
    Manage extractions programmatically
  </Card>

  <Card title="Webhooks" icon="webhook" href="/docs/post-call/polling-call-status-webhooks">
    Receive extraction data in real-time
  </Card>
</CardGroup>
