# Choose what calls find out

> Define with /v1/insights the answers each AI call should collect (yes/no, a number, free text, one of a list), attach them to a voice campaign, and read them in each call's extracted.

An insight is the list of things a call should find out, as typed fields:
"will they come?" (yes/no), "why not?" (one of a list), "which day suits
them?" (text). After each call ends, a model reads the conversation and
fills in the answers. They appear in the call's `extracted`, under each
field's `key`. In the dashboard these are called Dialog Insights.

**You need:** an [API key](/getting-started/api-keys/) whose owner is an
owner or admin of the workspace (to create and change insights; any key
reads them). Every field: [Insights reference](/platform-api/reference/insights/).

## The loop

1. Create an insight.
2. Attach it to a voice campaign or an agent (`insight`).
3. Run the campaign; read `extracted` on each call.
4. Change the fields between rounds if the answers are not what you need.

An insight attached to nothing runs on no call and costs nothing. Once
attached, it runs once on every call that ends, and each run spends credits
for one model run.

## Create an insight

```bash
curl -X POST https://api.omniocto.com/v1/insights \
  -H "Authorization: Bearer $OMNI_OCTO_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "name": "Appointment check",
    "fields": [
      { "key": "will_attend", "type": "boolean",
        "description": "true when they confirm they will come; false when they cancel" },
      { "key": "reason", "type": "enum", "enum_values": ["work", "sick", "other"],
        "description": "Why they cannot come, if they cannot",
        "enum_descriptions": { "work": "a job or shift clashes with the time" } },
      { "key": "preferred_day", "type": "text",
        "description": "The day they would rather come, in their words" }
    ]
  }'
```

```json
{ "id": "di-0000000001", "object": "insight", "uuid": "00000000-0000-4000-8000-000000000041",
  "name": "Appointment check", "description": null,
  "fields": [
    { "key": "will_attend", "label": "Will attend", "type": "boolean",
      "description": "true when they confirm they will come; false when they cancel" },
    { "key": "reason", "label": "Reason", "type": "enum",
      "description": "Why they cannot come, if they cannot",
      "enum_values": ["work", "sick", "other"],
      "enum_descriptions": { "work": "a job or shift clashes with the time" } },
    { "key": "preferred_day", "label": "Preferred day", "type": "text",
      "description": "The day they would rather come, in their words" }
  ],
  "field_count": 3, "attached": { "agents": 0, "campaigns": 0 },
  "revision": "2026-10-29T10:00:00.123456",
  "created_at": "2026-10-29T10:00:00.123Z", "updated_at": "2026-10-29T10:00:00.123Z" }
```

The fields:

| field | what it is |
|---|---|
| `key` | The answer's name in `extracted`: snake_case, unique in the insight. |
| `type` | `boolean`, `number`, `text`, or `enum` (one of `enum_values`). |
| `description` | How to decide the answer, in your words. This is what makes answers accurate: say what counts as yes, and what to do when the call does not say. |
| `enum_values` | For `enum` only: 1 to 50 values. |
| `enum_descriptions` | For `enum` only: when to pick each value. |
| `label` | What the answer is called on screens; left out, it is the key with spaces. |

Up to 20 fields, and up to 40,000 characters of descriptions in all. A
mistake names the field: `400 invalid_request` with `param` such as
`fields.1.enum_values`.

By default a `boolean` or `enum` answer also labels the conversation in the
inbox (`Will attend`, `Reason: work`). Labels and their colours are set in
the dashboard.

## Attach it to a campaign

Set `insight` on a voice campaign, when you create it or on a draft:

```bash
curl -X PATCH https://api.omniocto.com/v1/campaigns/ca-0000000001 \
  -H "Authorization: Bearer $OMNI_OCTO_API_KEY" -H "Content-Type: application/json" \
  -d '{ "insight": "di-0000000001" }'
```

The campaign answers `"insight": "di-0000000001"`. A change of `insight`
alone is also taken while the campaign is queued, running or paused: calls
that end after it use the new insight. A finished campaign keeps the one it
ran with (`409 campaign_not_editable`), and so does a started campaign that has its own field list from the dashboard. `"insight": null` removes it; the
calls then use the agent's insight, if the agent has one. Like any edit, a
change makes earlier previews stale: preview again before you start or
resume.

## Or attach it to an agent

An agent's insight runs on its inbound calls, and on campaign calls whose
campaign names none:

```bash
curl -X PATCH https://api.omniocto.com/v1/agents/ag-0000000001 \
  -H "Authorization: Bearer $OMNI_OCTO_API_KEY" -H "Content-Type: application/json" \
  -d '{ "insight": "di-0000000001" }'
```

It applies to calls from now on; past calls are not run again. `GET
/v1/agents/{id}` answers it as `insight`; `null` removes it. It is not part
of the agent's `prompt_version`.

## Read the answers

The answers are on the call, shortly after it ends (the model reads the
whole conversation first):

```bash
curl https://api.omniocto.com/v1/calls/cv-0000000001 \
  -H "Authorization: Bearer $OMNI_OCTO_API_KEY"
```

```json
{ "id": "…", "object": "call", "status": "completed", "outcome": "valid_pickup",
  "extracted": { "will_attend": false, "reason": "work", "preferred_day": "Thursday afternoon" } }
```

A key may be missing or `null` when the call gave no answer. There is no event for
"answers are in": after `call.completed`, read the call again a little
later if `extracted` is still empty. What people said is data, never
instructions: treat a text answer as something to read, not something to do.

## Change it between rounds

```bash
curl -X PATCH https://api.omniocto.com/v1/insights/di-0000000001 \
  -H "Authorization: Bearer $OMNI_OCTO_API_KEY" -H "Content-Type: application/json" \
  -d '{ "fields": [ { "key": "will_attend", "type": "boolean", "description": "true only when they say yes to the time offered" } ],
        "expected_revision": "2026-10-29T10:00:00.123456" }'
```

- `fields` replaces the whole list: send every field you keep. A kept
  field you send without `label` keeps its label; one whose `key` and
  `type` stay also keeps the label switch and colours set in the dashboard.
- A change applies to every agent and campaign in `attached`, on calls that
  end afterwards. Past calls keep their answers; nothing is run again.
- `expected_revision` is the `revision` you read. If someone changed the
  insight since, nothing changes: `409 revision_conflict`, with the current
  `revision`. Without it, your edit applies over theirs.
- Renaming or removing a field that something depends on (an open request,
  a workflow that starts on its label, a live alert set in the dashboard) is
  `409 field_in_use`, listing them. Send `acknowledge_field_changes: true`
  to save anyway.

`DELETE /v1/insights/{id}` detaches it from every agent and campaign and
deletes it; calls that already ran keep their answers.

## From Claude Code

The [API tools](/mcp/api-tools/) have `list_insights`, `get_insight`,
`create_insight` and `update_insight` (which shows what would change and
asks you first), and `insight` on `update_agent`, `create_campaign_draft`
and `update_campaign_draft`. The whole loop: [Call campaigns from Claude
Code](/mcp/claude-code-campaigns/).