Skip to contentSkip to navigationSkip to topbar
Page tools
Useful for sharing or LLM
Accelerate development with AI

On this page
Looking for more inspiration?Visit the

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 aggregates the numeric values your 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

how-it-works page anchor

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.

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.

(warning)

Registering a custom measure is permanent

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.


Common signals for custom measures

common-signals-for-custom-measures page anchor

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

SignalWhat it capturesExample output field
Lead scoreHow qualified a prospect isleadScore
QA scoreConversation quality against your own rubricagentScore
Compliance scoreShare of required disclosures actually delivereddisclosureScore
Escalation riskLikelihood the conversation needs a humanescalationRisk
Upsell propensityLikelihood a customer accepts an offerupsellPropensity

For more ideas about the signals a custom operator can extract, see Custom operator examples, which covers scenarios such as conversation scoring, lead qualification, escalation risk detection, and compliance detection.

(information)

Measures must be numeric

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.


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 and Conversation 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 and Structuring JSON schema.
  2. In Conversation Intelligence, attach the operator to an intelligence configuration. Operators run through configurations. See Create an intelligence configuration and Define rules.
  3. In Conversation Orchestrator, attach that intelligence configuration to your Orchestrator configuration, so the operator runs against your conversations. See Conversation Orchestrator.

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


Identify the operator and field

identify-the-operator-and-field page anchor

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:

1
{
2
"type": "object",
3
"properties": {
4
"leadScore": { "type": "number" },
5
"reasoning": { "type": "string" }
6
}
7
}

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

verify-the-operator-before-you-register page anchor

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

check-your-remaining-slots page anchor

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

1
curl -X GET "https://insights.twilio.com/v3/ControlPlane/ConversationInsights/CustomFieldMappings/Capacity" \
2
-u "$TWILIO_API_KEY:$TWILIO_API_SECRET"
1
{
2
"measure": {
3
"consumed": 1,
4
"remaining": 2
5
}
6
}

Register a custom measure

register-a-custom-measure page anchor

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

1
curl -X POST "https://insights.twilio.com/v3/ControlPlane/ConversationInsights/CustomFieldMappings" \
2
-u "$TWILIO_API_KEY:$TWILIO_API_SECRET" \
3
-H "Content-Type: application/json" \
4
-d '{
5
"category": "MEASURE",
6
"name": "LeadScore",
7
"description": "Lead quality score from the lead scoring operator",
8
"entityMetadata": {
9
"sourceType": "INTELLIGENCE_OPERATOR",
10
"operatorId": "intelligence_operator_01k6fc25s7epm9qtk8rszbv3q5",
11
"field": "leadScore"
12
}
13
}'

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:

1
{
2
"id": "convinsights_mapping_01m31967b7em08vh1pqgbam3mj",
3
"category": "MEASURE",
4
"name": "LeadScore",
5
"description": "Lead quality score from the lead scoring operator",
6
"entityMetadata": {
7
"sourceType": "INTELLIGENCE_OPERATOR",
8
"operatorId": "intelligence_operator_01k6fc25s7epm9qtk8rszbv3q5",
9
"operatorName": "Lead Scoring",
10
"field": "leadScore"
11
},
12
"createdAt": "2026-08-13T14:30:00Z",
13
"updatedAt": null
14
}

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

troubleshoot-a-failed-registration page anchor
ResponseWhat it meansWhat to do
400The field doesn't resolve against the operator's latest active version, or isn't numericConfirm the field name matches the operator's outputSchema exactly and that its type is number
403The credentials don't have permission to register custom fieldsUse an API key with access to the account that owns the operator
403MAPPING_CAPACITY_REACHED: all three custom measure slots are in useCheck your remaining slots. Registrations are permanent, so a used slot can't be freed
409The name is already registered to a different operatorChoose a different name. A name belongs permanently to the operator it was first registered against

Confirm your registered measures

confirm-your-registered-measures page anchor

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

1
curl -X GET "https://insights.twilio.com/v3/ControlPlane/ConversationInsights/CustomFieldMappings" \
2
-u "$TWILIO_API_KEY:$TWILIO_API_SECRET"
1
{
2
"mappings": [
3
{
4
"id": "convinsights_mapping_01m31967b7em08vh1pqgbam3mj",
5
"category": "MEASURE",
6
"name": "LeadScore",
7
"description": "Lead quality score from the lead scoring operator",
8
"entityMetadata": {
9
"sourceType": "INTELLIGENCE_OPERATOR",
10
"operatorId": "intelligence_operator_01k6fc25s7epm9qtk8rszbv3q5",
11
"operatorName": "Lead Scoring",
12
"field": "leadScore"
13
},
14
"createdAt": "2026-08-13T14:30:00Z",
15
"updatedAt": null
16
}
17
]
18
}

Your registered measures also appear in the Metadata resource response, alongside the built-in measures and dimensions.


Multiple results in one conversation

multiple-results-in-one-conversation page anchor

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:

MeasureRows for that conversationResult for that conversation
ConversationSummary.AgentScoreAvgOne row: 9090
OperatorResult.AgentScoreAvgThree rows: 70, 80, and 9080

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.

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.


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

average-a-score-by-channel page anchor

This example averages lead scores by channel, using the gold ConversationSummary cube so that each conversation contributes once:

1
{
2
"domain": "Conversations",
3
"query": {
4
"measures": ["ConversationSummary.LeadScoreAvg"],
5
"dimensions": ["ConversationSummary.Channels"],
6
"filters": [
7
{
8
"expressions": [
9
{
10
"op": "GT",
11
"field": "ConversationSummary.CreatedDate",
12
"values": ["2026-04-02"]
13
}
14
]
15
}
16
]
17
}
18
}

Combine a custom measure with other conversation data

combine-a-custom-measure-with-other-conversation-data page anchor

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:

1
{
2
"domain": "Conversations",
3
"query": {
4
"measures": ["ConversationSummary.AgentScoreAvg"],
5
"dimensions": [
6
"ConversationSummary.Channels",
7
"ConversationSummary.HasAIAgent"
8
],
9
"filters": [
10
{
11
"expressions": [
12
{
13
"op": "GT",
14
"field": "ConversationSummary.CreatedDate",
15
"values": ["2026-04-02"]
16
}
17
]
18
}
19
]
20
}
21
}

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

1
{
2
"domain": "Conversations",
3
"items": [
4
{
5
"ConversationSummary.Channels": "voice",
6
"ConversationSummary.HasAIAgent": false,
7
"ConversationSummary.AgentScoreAvg": "87.4"
8
},
9
{
10
"ConversationSummary.Channels": "voice",
11
"ConversationSummary.HasAIAgent": true,
12
"ConversationSummary.AgentScoreAvg": "79.1"
13
},
14
{
15
"ConversationSummary.Channels": "sms",
16
"ConversationSummary.HasAIAgent": true,
17
"ConversationSummary.AgentScoreAvg": "91.6"
18
}
19
],
20
"meta": {
21
"key": "items",
22
"nextToken": null,
23
"pageSize": 50,
24
"previousToken": null
25
}
26
}

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

query-the-individual-results-behind-a-number page anchor

To analyze the individual operator results rather than the conversation-level rollup, query the same measure on the silver OperatorResult cube as OperatorResult.LeadScoreAvg.

For the difference between the two cubes and guidance on which to use, see Core concepts.


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

Explore the following resources to learn more: