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

# Extraction

> Define the typed fields a conversation should yield, then read the structured data back.

A conversation is unstructured: turns of text back and forth. Extraction is
how you turn it into fields you can count, filter, and export. You declare a
**schema** on an initiative: the typed fields Boom should pull out of every
conversation it runs there. Boom fills that schema for each participant
once their conversation has enough signal to work with.

This works for any conversation, not only research. A support initiative can
declare `resolved` (was the issue fixed) and `issue_category` (which kind of
issue it was); a sales initiative can declare `objection` and `next_step`; an
onboarding initiative can declare `blocked_on`. Every closed conversation then
writes back the same fields, so you get one row per participant instead of a
transcript you'd have to read one at a time.

<Note>
  Extraction is scoped to one initiative. Each initiative has its own schema,
  and there's no shared, cross-initiative field catalog today.
</Note>

<Warning>
  Set the schema before you launch. A conversation is extracted against
  whichever schema was current when it ran, and the API cannot go back and
  re-extract a closed conversation under new fields. Add a field an hour after
  launching and the conversations from that first hour never carry it.
</Warning>

## Defining a schema

A schema is a map of field slugs to field definitions:

| Type       | What it captures                                       | When to use it                                                                                                                     |
| ---------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| `bool`     | Yes/no.                                                | A binary flag, e.g. whether the issue was resolved.                                                                                |
| `int`      | A whole number, optionally bounded by `min`/`max`.     | A count or amount mentioned in the conversation.                                                                                   |
| `enum`     | One value from a fixed list you declare.               | A single category the conversation falls into.                                                                                     |
| `enum[]`   | Zero or more values from that same fixed list.         | More than one label can legitimately apply.                                                                                        |
| `string`   | Free text, optionally capped at `maxLength`.           | A summary, reason, or quote in the customer's words.                                                                               |
| `string[]` | Zero or more free-text items, with no predefined list. | An open set the conversation surfaces (products mentioned, competitors named) that you don't want to constrain to a fixed catalog. |

Every field also carries:

| Property      | Required                   | Notes                                                                                                                                                                                               |
| ------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label`       | ✅                          | Display name, 1–80 characters.                                                                                                                                                                      |
| `description` | ✅                          | 1–500 characters. This is not just documentation: it's the instruction the model reads to decide what to extract for this field, so write it like you're briefing the model, not labeling a column. |
| `values`      | Only for `enum` / `enum[]` | 1–64 characters per value, at least one value, no duplicates.                                                                                                                                       |
| `min`, `max`  | Optional, `int` only       | Integer bounds.                                                                                                                                                                                     |
| `maxLength`   | Optional, `string` only    | Positive integer.                                                                                                                                                                                   |

The field slug (the map key) must be lowercase, start with a letter, and
contain only letters, digits, and underscores, up to 40 characters. `topic`,
`problem`, and `competitor` are reserved: Boom already writes its own
org-wide signals under those keys, so a custom schema can't declare them:
`extraction_schema_set` rejects the request with a `reserved_field` error.

### Applying a schema

<Tabs>
  <Tab title="MCP">
    `extraction_schema_set`: author or replace the schema in one call.
  </Tab>

  <Tab title="REST">
    `POST /api/v1/initiatives/{id}/extraction-schema`
  </Tab>
</Tabs>

```bash theme={null}
curl -X POST "https://www.useboom.ai/api/v1/initiatives/init_123/extraction-schema" \
  -H "Authorization: Bearer $BOOM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "schema": {
      "fields": {
        "resolved": {
          "type": "bool",
          "label": "Resolved",
          "description": "True if the customer confirmed their issue was fixed by the end of the conversation, false otherwise."
        },
        "issue_category": {
          "type": "enum",
          "label": "Issue category",
          "description": "The category that best matches the customer'\''s issue.",
          "values": ["billing", "bug", "how_to", "cancellation", "other"]
        },
        "summary": {
          "type": "string",
          "label": "Summary",
          "description": "One sentence summarizing what the customer needed, in their own words.",
          "maxLength": 280
        }
      }
    },
    "notes": "v1: support triage fields"
  }'
```

This replaces the initiative's schema wholesale, so always submit the
complete set of fields, not a diff. The response tells you whether it
actually changed anything:

```json theme={null}
{
  "initiativeId": "init_123",
  "version": 3,
  "changed": true,
  "fieldCount": 3,
  "notes": "v1: support triage fields",
  "createdAt": "2026-07-24T10:00:00.000Z"
}
```

`changed: false` means the schema you submitted was identical to what was
already current: no new version was created, and the `notes` you sent were
not recorded (the response returns the prior version's notes instead).

Read the current schema back with `extraction_schema_get` /
`GET /api/v1/initiatives/{id}/extraction-schema`. It returns the schema in
the same shape `extraction_schema_set` accepts, so you can fetch it, edit a
field, and set it back. `hasSchema: false` and `schema: null` mean the
initiative has none configured yet.

<Note>
  Setting a schema reshapes how every conversation on the initiative gets
  analyzed, so it is not a casual change. Over MCP it requires the signed-in
  user to be an org admin. Over REST it takes a valid organization API key, and
  any valid key can make this call: treat a key as full access to your
  organization and scope who holds it accordingly.
</Note>

## Reading the data back

<CardGroup cols={2}>
  <Card title="Per participant" icon="user">
    `GET /api/v1/initiatives/{id}/participants/{participantId}` returns a
    `values` array: one entry per captured field, each with `label`,
    `valueKind`, a display `value`, and (when available) the `sourceQuote`
    and `confidence` behind it.
  </Card>

  <Card title="Listed across participants" icon="list">
    `GET /api/v1/initiatives/{id}/participants` (paginated) returns each
    participant's `values` as an object keyed by field slug, each value an
    array of display strings, plus `capturedAt`.
  </Card>

  <Card title="In aggregate" icon="chart-bar">
    `GET /api/v1/initiatives/{id}/data/summary` (`initiatives_summary`)
    returns `participantCount` plus one entry per variable: `coverage`
    (`answered`/`eligible`), and depending on type, an `average` (numeric
    fields), a `distribution` (categorical fields), or `samples` (text
    fields).
  </Card>
</CardGroup>

<Warning>
  These three endpoints return **every** variable captured on the
  initiative, not only the fields in your schema. Boom's own default
  signals and, where configured, guiding-question answers come back
  alongside them. Match on the field slugs (and, in the aggregate summary,
  the `source` field) to isolate what you defined.
</Warning>

## Using extracted values later

Once a schema is set (that's what "publishes" it), its fields become
addressable in journey message templates as `engagement.extracted.<slug>`,
for example `engagement.extracted.issue_category`. This only resolves once
the upstream conversation step that produces it has actually run; before
that, the placeholder goes out blank. See
[Template variables](/template-variables) for the full binding syntax and
the trigger-by-trigger resolution matrix.

## Rules and limits

* **A schema needs at least one field.** An empty `fields` map is rejected.
* **Naming is the only structural limit.** No cap on how many fields a
  schema declares, but the slug rules above are enforced.
* **Changing a schema doesn't retroactively update past extractions.**
  Engagements already extracted keep the values produced by whichever
  schema version was current when they ran. The new version only applies to
  conversations extracted after the change. Re-running extraction against
  old, already-closed conversations under a new schema is a dashboard/CLI
  operation today, not something the public API or MCP exposes.
* **Extraction only runs on conversations with enough signal**: at least 3
  total messages and at least 1 inbound message. Below that bar, a
  conversation is skipped rather than filled in with empty values. Plan for this
  when the audience is people who already went quiet: whoever never replies
  produces no fields at all, so coverage on a win-back reflects who answered,
  not who you reached.
* **Repeat triggers don't repeat the LLM call.** Boom hashes the transcript
  together with the schema; if neither changed since the last extraction,
  it skips the call. Editing the schema changes the hash, so the next time
  that conversation closes (or a manual re-extract fires), it re-runs
  against the new fields.
* **Scalar fields (`bool`, `int`, `enum`, `string`) always get a value.**
  Every field in the schema is required in the model's output. There's no
  built-in "unknown" or null state for these types; the model does its best
  with what the conversation gave it, confirming or updating any
  previously extracted value. List fields (`enum[]`, `string[]`) are the
  exception: the model is instructed to emit an empty array when the
  conversation gives no signal.

## Related

<CardGroup cols={2}>
  <Card title="Template variables" icon="brackets-curly" href="/template-variables">
    Bind `engagement.extracted.*` into a WhatsApp template placeholder.
  </Card>

  <Card title="One uniform surface" icon="layers" href="/one-surface">
    How the same capability is exposed identically on REST and MCP.
  </Card>

  <Card title="Use MCP" icon="plug" href="/use-mcp">
    Connect an AI tool to read and set extraction schemas conversationally.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference/overview">
    Every Initiatives, Journeys, and Segments endpoint.
  </Card>
</CardGroup>
