---
"@context": https://schema.org
"@type": TechArticle
"@id": https://www.twilio.com/docs/conversations/intelligence/conversation-insights/define-custom-fields#article
headline: Define and use custom fields
description: Register a numeric field from a Conversation Intelligence custom operator as a custom measure, then aggregate it across all your conversations with Conversation Insights.
url: https://www.twilio.com/docs/conversations/intelligence/conversation-insights/define-custom-fields
inLanguage: en
dateModified: 2026-10-09T13:16:15.000Z
author:
  "@type": Organization
  name: Twilio Developer Education Team
publisher:
  "@type": Organization
  name: Twilio
---

# Define and use custom fields

Custom fields let you extend the Conversation Insights data model with the signals that matter to your business, and then query them the same way you query built-in data.

[Conversation Insights](/docs/conversations/intelligence/conversation-insights) aggregates the numeric values your [custom language operators](/docs/conversations/intelligence/create-custom-language-operators) produce. Register a numeric field from a custom operator as a custom measure, and its value becomes queryable across your entire conversation volume: average lead scores by channel, compare QA ratings between AI and human agents, or track a compliance score week over week, with no separate pipeline to build.

## How it works

You register every custom field under a **category**, which determines how you can use it in a query. Measures, the numeric values you aggregate, are the first supported category, and the rest of this page covers registering one. Support for custom dimensions, the attributes you filter and group by, is planned. For the difference between measures and dimensions, see [Core concepts](/docs/conversations/intelligence/conversation-insights/core-concepts).

Registration maps a name you choose to a source field in a custom operator's output. Conversation Insights validates that the source field resolves against the operator's latest active version, then makes the field available in both cubes:

* `ConversationSummary.<Name><Aggregation>` on the gold cube, at one row per conversation
* `OperatorResult.<Name><Aggregation>` on the silver cube, at operator result grain

`<Aggregation>` is one of `Sum`, `Avg`, `Min`, or `Max`, so a measure registered as `LeadScore` is queryable as `ConversationSummary.LeadScoreAvg`, `OperatorResult.LeadScoreMax`, and so on. Conversations where the operator didn't run return `null` for that field.

Registration applies going forward only. A custom measure captures values from operator runs that happen after you register it, and existing conversations aren't backfilled, so plan to register a measure before the period you want to report on.

```mermaid {title="From operator output to an aggregated measure"}
flowchart LR
    subgraph CINTEL[Conversation Intelligence]
        direction TB
        OP["Custom operator<br/><small>(JSON output)</small>"]
        CFG[Intelligence<br/>configuration]
        OP ~~~ CFG
    end
    subgraph COR[Conversation Orchestrator]
        RUN["Operator runs on<br/>your conversations"]
    end
    subgraph CI[Conversation Insights]
        direction TB
        REG["Register the field<br/>as a custom measure"]
        QRY["Query it across all<br/>your conversations"]
        REG ~~~ QRY
    end
    CINTEL --> COR
    COR --> CI
```

> \[!WARNING]
>
> Your account has three custom measure slots. Registering a measure binds a name to a specific operator, and that binding can't be removed or reassigned to a different operator. Attempting to reuse a name for a different operator returns a `409` response.
>
> Register measures you intend to keep. Iterating on an operator costs nothing, so confirm it produces the value you expect before you register it. See [Verify the operator before you register](#verify-the-operator-before-you-register).

## Common signals for custom measures

A custom measure can be any numeric value your operator produces. Common choices include the following.

| Signal            | What it captures                                 | Example output field |
| ----------------- | ------------------------------------------------ | -------------------- |
| Lead score        | How qualified a prospect is                      | `leadScore`          |
| QA score          | Conversation quality against your own rubric     | `agentScore`         |
| Compliance score  | Share of required disclosures actually delivered | `disclosureScore`    |
| Escalation risk   | Likelihood the conversation needs a human        | `escalationRisk`     |
| Upsell propensity | Likelihood a customer accepts an offer           | `upsellPropensity`   |

For more ideas about the signals a custom operator can extract, see [Custom operator examples](/docs/conversations/intelligence/custom-language-operator-examples), which covers scenarios such as conversation scoring, lead qualification, escalation risk detection, and compliance detection.

> \[!NOTE]
>
> You can register only numeric fields as measures. If the signal you want is currently a string, such as a score returned as `"87"`, change that field's type to `number` in your operator's `outputSchema` before you register it. Conversation Insights can't aggregate string fields, including classification labels.

## Prerequisites

Custom fields read from custom operator output, so the operator must already exist and be running against your conversations before you register a field.

Creating and running operators is the job of [Conversation Intelligence](/docs/conversations/intelligence) and [Conversation Orchestrator](/docs/conversations/orchestrator). Conversation Insights only aggregates what those products produce, so complete all three of the following steps outside Conversation Insights first.

1. **In Conversation Intelligence, create a custom operator with a JSON output format.** Custom measures require a numeric field in the operator's output, so set `outputFormat` to `JSON` and include the numeric field in your `outputSchema`. See [Create custom operators](/docs/conversations/intelligence/create-custom-language-operators) and [Structuring JSON schema](/docs/conversations/intelligence/create-custom-language-operators#structuring-json-schema).
2. **In Conversation Intelligence, attach the operator to an intelligence configuration.** Operators run through configurations. See [Create an intelligence configuration](/docs/conversations/intelligence/create-intelligence-configuration) and [Define rules](/docs/conversations/intelligence/define-rules).
3. **In Conversation Orchestrator, attach that intelligence configuration to your Orchestrator configuration**, so the operator runs against your conversations. See [Conversation Orchestrator](/docs/conversations/orchestrator).

Once the operator is producing results, you can register one of its numeric output fields in Conversation Insights.

## Identify the operator and field

You need two values to register a measure: the operator's identifier and the name of the numeric field in its output.

For example, a lead scoring operator might use the following output schema:

```json
{
  "type": "object",
  "properties": {
    "leadScore": { "type": "number" },
    "reasoning": { "type": "string" }
  }
}
```

Here the field to register is `leadScore`. The `reasoning` field is a string, so it can't be a measure.

Conversation Insights validates the field against the operator's **latest active version**. If you change the operator's output schema and remove the field, later registrations for that field fail.

### Verify the operator before you register

Because a registration permanently consumes one of your three slots, confirm the operator is producing the value you expect before you register it. Do this in Conversation Intelligence rather than in Conversation Insights:

1. Run the operator against a few representative conversations.
2. Inspect the operator results and confirm the field appears with a numeric value, not a string or a null.
3. Confirm the field name in the results matches the name in your `outputSchema` exactly, including capitalization.

Iterating on an operator costs nothing. Only registration consumes a slot.

## Check your remaining slots

Before registering, check how many of your three slots are still available. Send a `GET` request to the Capacity subresource:

```bash
curl -X GET "https://insights.twilio.com/v3/ControlPlane/ConversationInsights/CustomFieldMappings/Capacity" \
  -u "$TWILIO_API_KEY:$TWILIO_API_SECRET"
```

```json
{
  "measure": {
    "consumed": 1,
    "remaining": 2
  }
}
```

## Register a custom measure

Send a `POST` request to the CustomFieldMappings resource with the name you want to use, an optional description, the operator identifier, and the field:

```bash
curl -X POST "https://insights.twilio.com/v3/ControlPlane/ConversationInsights/CustomFieldMappings" \
  -u "$TWILIO_API_KEY:$TWILIO_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "category": "MEASURE",
    "name": "LeadScore",
    "description": "Lead quality score from the lead scoring operator",
    "entityMetadata": {
      "sourceType": "INTELLIGENCE_OPERATOR",
      "operatorId": "intelligence_operator_01k6fc25s7epm9qtk8rszbv3q5",
      "field": "leadScore"
    }
  }'
```

The `name` you choose becomes the base of the measure names you query, so pick something you want to see in your reports and dashboards. Names can contain only letters, numbers, and underscores.

The `description` is optional free text of up to 150 characters. Use it to record what the measure represents, so others on your team can identify it when they list your registered measures. The description is for display only and isn't part of the uniqueness check, so you can change it by sending the registration again with a new description.

A successful registration returns `201`:

```json
{
  "id": "convinsights_mapping_01m31967b7em08vh1pqgbam3mj",
  "category": "MEASURE",
  "name": "LeadScore",
  "description": "Lead quality score from the lead scoring operator",
  "entityMetadata": {
    "sourceType": "INTELLIGENCE_OPERATOR",
    "operatorId": "intelligence_operator_01k6fc25s7epm9qtk8rszbv3q5",
    "operatorName": "Lead Scoring",
    "field": "leadScore"
  },
  "createdAt": "2026-08-13T14:30:00Z",
  "updatedAt": null
}
```

The measure is queryable as soon as operator runs start producing values for it. Because registration isn't retroactive, expect empty results until the operator runs again on new conversations.

Registration is idempotent for the same name and operator. Sending the same request again returns `200` with the existing mapping rather than consuming another slot. Sending the same `name` with a different `operatorId` returns `409`, because a name belongs permanently to the operator it was first registered against.

### Troubleshoot a failed registration

| Response | What it means                                                                            | What to do                                                                                                            |
| -------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `400`    | The field doesn't resolve against the operator's latest active version, or isn't numeric | Confirm the field name matches the operator's `outputSchema` exactly and that its type is `number`                    |
| `403`    | The credentials don't have permission to register custom fields                          | Use an API key with access to the account that owns the operator                                                      |
| `403`    | `MAPPING_CAPACITY_REACHED`: all three custom measure slots are in use                    | [Check your remaining slots](#check-your-remaining-slots). Registrations are permanent, so a used slot can't be freed |
| `409`    | The name is already registered to a different operator                                   | Choose a different name. A name belongs permanently to the operator it was first registered against                   |

## Confirm your registered measures

To list every custom measure registered for your account, send a `GET` request to the CustomFieldMappings resource:

```bash
curl -X GET "https://insights.twilio.com/v3/ControlPlane/ConversationInsights/CustomFieldMappings" \
  -u "$TWILIO_API_KEY:$TWILIO_API_SECRET"
```

```json
{
  "mappings": [
    {
      "id": "convinsights_mapping_01m31967b7em08vh1pqgbam3mj",
      "category": "MEASURE",
      "name": "LeadScore",
      "description": "Lead quality score from the lead scoring operator",
      "entityMetadata": {
        "sourceType": "INTELLIGENCE_OPERATOR",
        "operatorId": "intelligence_operator_01k6fc25s7epm9qtk8rszbv3q5",
        "operatorName": "Lead Scoring",
        "field": "leadScore"
      },
      "createdAt": "2026-08-13T14:30:00Z",
      "updatedAt": null
    }
  ]
}
```

Your registered measures also appear in the [Metadata resource](/docs/conversations/intelligence/conversation-insights/aggregate-data#discover-available-data) response, alongside the built-in measures and dimensions.

## Multiple results in one conversation

An operator can produce more than one result for the same conversation, such as a real-time operator that re-evaluates as the conversation progresses. The two cubes handle that differently, and the difference shows up in your numbers.

* On the gold `ConversationSummary` cube, the **most recent result wins**. The conversation carries a single value for the measure, reflecting the state of the conversation at close rather than a history of runs.
* On the silver `OperatorResult` cube, **each result keeps its own row**, so aggregations span every run.

Consider a QA operator that runs three times during one conversation, returning `70`, then `80`, then `90`:

| Measure                             | Rows for that conversation       | Result for that conversation |
| ----------------------------------- | -------------------------------- | ---------------------------- |
| `ConversationSummary.AgentScoreAvg` | One row: `90`                    | `90`                         |
| `OperatorResult.AgentScoreAvg`      | Three rows: `70`, `80`, and `90` | `80`                         |

The summary row is a snapshot written when the conversation closes, so a result that arrives after that point doesn't appear on the gold cube, and updating the row afterward is planned. If your operator runs after the conversation ends, query the measure on the silver `OperatorResult` cube, which records every result regardless of when it lands. See [How the summary row is built](/docs/conversations/intelligence/conversation-insights/core-concepts#how-the-summary-row-is-built).

An average taken across your conversations therefore uses only each conversation's final score on the gold cube, and every individual score on the silver cube. Neither is wrong: use the gold cube to ask about conversations, where each conversation counts once no matter how many times an operator ran, and the silver cube to ask about the operator results themselves.

Because you register one specific field, there is no ambiguity about which value in the operator's output becomes the measure.

## Query a custom measure

Once registered, a custom measure behaves like any other measure. Choose an aggregation by adding its suffix to the measure name: `Sum`, `Avg`, `Min`, or `Max`. For example, `ConversationSummary.LeadScoreAvg` returns the average lead score. To count conversations, use `ConversationSummary.Count`.

### Average a score by channel

This example averages lead scores by channel, using the gold [`ConversationSummary`](/docs/conversations/intelligence/conversation-insights/core-concepts#conversationsummary-cube) cube so that each conversation contributes once:

```json
{
  "domain": "Conversations",
  "query": {
    "measures": ["ConversationSummary.LeadScoreAvg"],
    "dimensions": ["ConversationSummary.Channels"],
    "filters": [
      {
        "expressions": [
          {
            "op": "GT",
            "field": "ConversationSummary.CreatedDate",
            "values": ["2026-04-02"]
          }
        ]
      }
    ]
  }
}
```

### Combine a custom measure with other conversation data

Custom measures pay off most on the gold cube, because your measure sits on the same row as everything else known about that conversation. You can slice a score your own operator produced by attributes that come from a different product entirely.

This example averages an `AgentScore` measure by both channel and whether an AI agent took part. The score comes from your custom operator in Conversation Intelligence, while the channel and the `HasAIAgent` flag come from Conversation Orchestrator:

```json
{
  "domain": "Conversations",
  "query": {
    "measures": ["ConversationSummary.AgentScoreAvg"],
    "dimensions": [
      "ConversationSummary.Channels",
      "ConversationSummary.HasAIAgent"
    ],
    "filters": [
      {
        "expressions": [
          {
            "op": "GT",
            "field": "ConversationSummary.CreatedDate",
            "values": ["2026-04-02"]
          }
        ]
      }
    ]
  }
}
```

The response returns one row per combination, so you can read your QA score across both axes at once:

```json
{
  "domain": "Conversations",
  "items": [
    {
      "ConversationSummary.Channels": "voice",
      "ConversationSummary.HasAIAgent": false,
      "ConversationSummary.AgentScoreAvg": "87.4"
    },
    {
      "ConversationSummary.Channels": "voice",
      "ConversationSummary.HasAIAgent": true,
      "ConversationSummary.AgentScoreAvg": "79.1"
    },
    {
      "ConversationSummary.Channels": "sms",
      "ConversationSummary.HasAIAgent": true,
      "ConversationSummary.AgentScoreAvg": "91.6"
    }
  ],
  "meta": {
    "key": "items",
    "nextToken": null,
    "pageSize": 50,
    "previousToken": null
  }
}
```

Answering the same question from the silver cubes would take one query for the operator results, another for participant data, and a join in your own application on `ConversationId`.

### Query the individual results behind a number

To analyze the individual operator results rather than the conversation-level rollup, query the same measure on the silver [`OperatorResult`](/docs/conversations/intelligence/conversation-insights/core-concepts#operatorresult-cube) cube as `OperatorResult.LeadScoreAvg`.

For the difference between the two cubes and guidance on which to use, see [Core concepts](/docs/conversations/intelligence/conversation-insights/core-concepts).

## Limits

* Your account has three custom measure slots.
* A registration is permanent. A name can't be deleted or reassigned to a different operator.
* You can register only numeric fields as measures. String fields, including classification labels, aren't eligible.
* Only custom operators with an `outputFormat` of `JSON` produce fields eligible for registration.
* Registration isn't retroactive. Only operator runs after registration contribute values, and existing conversations aren't backfilled.
* Conversations where the operator didn't run return `null` for the measure.

## Next steps

Explore the following resources to learn more:

* [Core concepts](/docs/conversations/intelligence/conversation-insights/core-concepts): Understand cubes, measures, and dimensions.
* [Aggregate conversation data](/docs/conversations/intelligence/conversation-insights/aggregate-data): Run queries against the Metadata and Query resources.
* [Query syntax](/docs/conversations/intelligence/conversation-insights/query-syntax): Build queries with filters, aggregations, and time granularity.
