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.
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 conversationOperatorResult.<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.
Registering a custom measure is permanent
Your account has three custom measure slots by default. Registering a measure binds a name to a specific operator. That binding can't be removed or reassigned to a different operator. A name is unique across your account, so registering a name that already exists returns a 409 response. You can later change which output field the measure reads, but not its name, category, or operator.
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.
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, which covers scenarios such as conversation scoring, lead qualification, escalation risk detection, and compliance detection.
Measures must be numeric
You can register only number or integer 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.
- 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
outputFormattoJSONand include the numeric field in youroutputSchema. See Create custom operators and Structuring JSON schema. - In Conversation Intelligence, attach the operator to an intelligence configuration. Operators run through configurations. See Create an intelligence configuration and Define rules.
- 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.
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.
The field value is a path into the operator's output. It can be a top-level property, such as leadScore, or a property one level down, such as scores.leadScore. Use one or two segments separated by ., and only letters, numbers, and underscores in each segment. Twilio doesn't support array indexing, such as items[0].
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 and updates for that field fail.
Because a registration permanently consumes one of your custom measure slots, confirm the operator is producing the value you expect before you register it. Do this in Conversation Intelligence rather than in Conversation Insights:
- Run the operator against a few representative conversations.
- Inspect the operator results and confirm the field appears with a numeric value, not a string or a null.
- Confirm the field name in the results matches the name in your
outputSchemaexactly, including capitalization.
Iterating on an operator costs nothing. Only registration consumes a slot.
Send a POST request to the CustomFieldMappings resource with the name you want to use, an optional description, and an entityMetadata object that identifies the operator and field. Set entityMetadata.sourceType to INTELLIGENCE_OPERATOR and put the operator details in the intelligenceOperator property:
1// Download the helper library from https://www.twilio.com/docs/node/install2const twilio = require("twilio"); // Or, for ESM: import twilio from "twilio";34// Find your Account SID at twilio.com/console5// Provision API Keys at twilio.com/console/runtime/api-keys6// and set the environment variables. See http://twil.io/secure7// For local testing, you can use your Account SID and Auth token8const accountSid = process.env.TWILIO_ACCOUNT_SID;9const apiKey = process.env.TWILIO_API_KEY;10const apiSecret = process.env.TWILIO_API_SECRET;11const client = twilio(apiKey, apiSecret, { accountSid: accountSid });1213async function createCustomFieldMapping() {14const customFieldMapping =15await client.insights.v3.customFieldMappings.create({16category: "MEASURE",17name: "LeadScore",18description: "Lead quality score from the lead scoring operator",19entityMetadata: {20sourceType: "INTELLIGENCE_OPERATOR",21intelligenceOperator: {22operatorId: "intelligence_operator_01k6fc25s7epm9qtk8rszbv3q5",23operatorName: "Lead Scoring",24field: "leadScore",25},26},27});2829console.log(customFieldMapping.id);30}3132createCustomFieldMapping();
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, and you can't change a name after you register it.
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.
The operatorName is optional and for display only. Conversation Insights stores it as you supply it and doesn't validate it against the operator.
A successful registration returns 201 and an ETag response header that you can use when you update the measure:
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"intelligenceOperator": {9"operatorId": "intelligence_operator_01k6fc25s7epm9qtk8rszbv3q5",10"operatorName": "Lead Scoring",11"field": "leadScore"12}13},14"createdAt": "2026-08-13T14:30:00Z",15"updatedAt": null16}
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 create-only. Names are unique across your account, so sending a name that is already registered returns 409 and changes nothing, whether or not the operator or field match. The error response includes the existing mapping's id in params.id. To change an existing measure, update it instead of registering it again.
| Response | What it means | What to do |
|---|---|---|
400 | The request is invalid. For example, a required property is missing, the operator has no active version, it doesn't have an outputFormat of JSON, or the field doesn't resolve to a number or integer property in the operator's latest active version | Confirm the field path matches the operator's outputSchema exactly, that its type is number or integer, and that entityMetadata carries the intelligenceOperator property |
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 | The account has reached its custom mapping capacity for the MEASURE category: all of your custom measure slots are in use | Registrations are permanent, so a used slot can't be freed |
409 | A measure with this name is already registered for your account | Choose a different name, or update the existing measure using the id in params.id |
503 | Conversation Insights couldn't retrieve the operator details it needs to validate the request. No measure was created | Retry after the number of seconds in the Retry-After response header |
To list the custom measures registered for your account, send a GET request to the CustomFieldMappings resource. The response is paginated. Use the optional pageSize query parameter to limit the number of measures returned per page:
1// Download the helper library from https://www.twilio.com/docs/node/install2const twilio = require("twilio"); // Or, for ESM: import twilio from "twilio";34// Find your Account SID at twilio.com/console5// Provision API Keys at twilio.com/console/runtime/api-keys6// and set the environment variables. See http://twil.io/secure7// For local testing, you can use your Account SID and Auth token8const accountSid = process.env.TWILIO_ACCOUNT_SID;9const apiKey = process.env.TWILIO_API_KEY;10const apiSecret = process.env.TWILIO_API_SECRET;11const client = twilio(apiKey, apiSecret, { accountSid: accountSid });1213async function listCustomFieldMappings() {14const customFieldMappings = await client.insights.v3.customFieldMappings.list(15{ limit: 20 },16);1718customFieldMappings.forEach((c) => console.log(c.id));19}2021listCustomFieldMappings();
1{2"items": [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"intelligenceOperator": {11"operatorId": "intelligence_operator_01k6fc25s7epm9qtk8rszbv3q5",12"operatorName": "Lead Scoring",13"field": "leadScore"14}15},16"createdAt": "2026-08-13T14:30:00Z",17"updatedAt": null18}19],20"meta": {21"key": "items",22"nextToken": null,23"pageSize": 50,24"previousToken": null25}26}
To fetch the next page, pass the meta.nextToken value from the response as the pageToken query parameter. On the last page, meta.nextToken is null.
Your registered measures also appear in the Metadata resource response, alongside the built-in measures and dimensions.
To retrieve a single custom measure, send a GET request to the CustomFieldMappings resource with the measure's id:
1// Download the helper library from https://www.twilio.com/docs/node/install2const twilio = require("twilio"); // Or, for ESM: import twilio from "twilio";34// Find your Account SID at twilio.com/console5// Provision API Keys at twilio.com/console/runtime/api-keys6// and set the environment variables. See http://twil.io/secure7// For local testing, you can use your Account SID and Auth token8const accountSid = process.env.TWILIO_ACCOUNT_SID;9const apiKey = process.env.TWILIO_API_KEY;10const apiSecret = process.env.TWILIO_API_SECRET;11const client = twilio(apiKey, apiSecret, { accountSid: accountSid });1213async function fetchCustomFieldMapping() {14const customFieldMapping = await client.insights.v315.customFieldMappings("convinsights_mapping_01m31967b7em08vh1pqgbam3mj")16.fetch();1718console.log(customFieldMapping.id);19}2021fetchCustomFieldMapping();
The response body has the same shape as the registration response. The ETag response header identifies the measure's current version. A 404 response means no measure with that id exists for your account.
If you change an operator's output schema, or you registered the wrong field, you can point an existing measure at a different output field without registering a new measure. Send a PATCH request to the CustomFieldMappings resource with the measure's id. Set entityMetadata.sourceType to the measure's current source type, and put the new path in entityMetadata.intelligenceOperator.field:
1// Download the helper library from https://www.twilio.com/docs/node/install2const twilio = require("twilio"); // Or, for ESM: import twilio from "twilio";34// Find your Account SID at twilio.com/console5// Provision API Keys at twilio.com/console/runtime/api-keys6// and set the environment variables. See http://twil.io/secure7// For local testing, you can use your Account SID and Auth token8const accountSid = process.env.TWILIO_ACCOUNT_SID;9const apiKey = process.env.TWILIO_API_KEY;10const apiSecret = process.env.TWILIO_API_SECRET;11const client = twilio(apiKey, apiSecret, { accountSid: accountSid });1213async function patchCustomFieldMapping() {14const customFieldMapping = await client.insights.v315.customFieldMappings("convinsights_mapping_01m31967b7em08vh1pqgbam3mj")16.patch({17entityMetadata: {18sourceType: "INTELLIGENCE_OPERATOR",19intelligenceOperator: {20field: "scores.leadScore",21},22},23});2425console.log(customFieldMapping.id);26}2728patchCustomFieldMapping();
A successful update returns 200 with the updated measure, sets updatedAt, and returns a new ETag header:
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"intelligenceOperator": {9"operatorId": "intelligence_operator_01k6fc25s7epm9qtk8rszbv3q5",10"operatorName": "Lead Scoring",11"field": "scores.leadScore"12}13},14"createdAt": "2026-08-13T14:30:00Z",15"updatedAt": "2026-09-30T10:00:00Z"16}
Keep the following in mind when you update a measure:
- You can only change the
entityMetadata.intelligenceOperator.field. You can't update thename,category,description,operatorId, andoperatorName, and sending any of them returns400. - The new
fieldfollows the same rules as at registration: one or two.-separated segments that resolve to anumberorintegerproperty in the operator's latest active version. - Values already captured aren't recomputed. Operator results processed after the update use the new
field. - Sending a
fieldequal to the current value returns200without changing the measure. - The optional
If-Matchheader guards against concurrent updates. Set it to theETagyou got from aGET,POST, or previousPATCHresponse. If the measure has changed since thatETagwas issued, the request returns412and changes nothing.
| Response | What it means | What to do |
|---|---|---|
400 | The body is empty, includes a property that can't be updated, has a sourceType that doesn't match the measure, or the new field doesn't resolve to a number or integer property | Send only entityMetadata.intelligenceOperator.field, and confirm the path against the operator's outputSchema |
404 | No measure with that id exists for your account | List your measures to find the correct id |
409 | Another request modified the measure while yours was being applied | Fetch the measure again and retry |
412 | The If-Match value doesn't match the measure's current ETag | Fetch the measure to get its current ETag, then retry |
503 | Conversation Insights couldn't retrieve the operator details it needs to validate the update. The measure wasn't changed | Retry after the number of seconds in the Retry-After response header |
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
ConversationSummarycube, 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
OperatorResultcube, 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.
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.
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}
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": null25}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.
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 by default.
- A registration is permanent. A name can't be deleted, renamed, or reassigned to a different operator. You can change only the output
fieldit reads. - Names are unique across your account. Registering an existing name returns a
409response. - You can register only numeric fields (
numberorinteger) as measures. String fields, including classification labels, aren't eligible. - Only custom operators with an
outputFormatofJSONproduce fields eligible for registration. - Registration isn't retroactive. Only operator runs after registration contribute values. Existing conversations aren't backfilled. Updating a measure's
fieldapplies only to operator results processed afterward. - The
fieldpath can have, at most, two segments, such asscores.leadScore. Twilio doesn't support array indexing. - Conversations where the operator didn't run return
nullfor the measure.
Explore the following resources to learn more:
- Core concepts: Understand cubes, measures, and dimensions.
- Aggregate conversation data: Run queries against the Metadata and Query resources.
- Query syntax: Build queries with filters, aggregations, and time granularity.