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


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

  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.

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.

Verify the operator before you register

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

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:

  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.


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, 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:

Register a custom measureLink to code sample: Register a custom measure
1
// Download the helper library from https://www.twilio.com/docs/node/install
2
const twilio = require("twilio"); // Or, for ESM: import twilio from "twilio";
3
4
// Find your Account SID at twilio.com/console
5
// Provision API Keys at twilio.com/console/runtime/api-keys
6
// and set the environment variables. See http://twil.io/secure
7
// For local testing, you can use your Account SID and Auth token
8
const accountSid = process.env.TWILIO_ACCOUNT_SID;
9
const apiKey = process.env.TWILIO_API_KEY;
10
const apiSecret = process.env.TWILIO_API_SECRET;
11
const client = twilio(apiKey, apiSecret, { accountSid: accountSid });
12
13
async function createCustomFieldMapping() {
14
const customFieldMapping =
15
await client.insights.v3.customFieldMappings.create({
16
category: "MEASURE",
17
name: "LeadScore",
18
description: "Lead quality score from the lead scoring operator",
19
entityMetadata: {
20
sourceType: "INTELLIGENCE_OPERATOR",
21
intelligenceOperator: {
22
operatorId: "intelligence_operator_01k6fc25s7epm9qtk8rszbv3q5",
23
operatorName: "Lead Scoring",
24
field: "leadScore",
25
},
26
},
27
});
28
29
console.log(customFieldMapping.id);
30
}
31
32
createCustomFieldMapping();

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": null
16
}

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.

Troubleshoot a failed registration

troubleshoot-a-failed-registration page anchor
ResponseWhat it meansWhat to do
400The 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 versionConfirm the field path matches the operator's outputSchema exactly, that its type is number or integer, and that entityMetadata carries the intelligenceOperator property
403The credentials don't have permission to register custom fieldsUse an API key with access to the account that owns the operator
403The account has reached its custom mapping capacity for the MEASURE category: all of your custom measure slots are in useRegistrations are permanent, so a used slot can't be freed
409A measure with this name is already registered for your accountChoose a different name, or update the existing measure using the id in params.id
503Conversation Insights couldn't retrieve the operator details it needs to validate the request. No measure was createdRetry after the number of seconds in the Retry-After response header

Confirm your registered measures

confirm-your-registered-measures page anchor

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/install
2
const twilio = require("twilio"); // Or, for ESM: import twilio from "twilio";
3
4
// Find your Account SID at twilio.com/console
5
// Provision API Keys at twilio.com/console/runtime/api-keys
6
// and set the environment variables. See http://twil.io/secure
7
// For local testing, you can use your Account SID and Auth token
8
const accountSid = process.env.TWILIO_ACCOUNT_SID;
9
const apiKey = process.env.TWILIO_API_KEY;
10
const apiSecret = process.env.TWILIO_API_SECRET;
11
const client = twilio(apiKey, apiSecret, { accountSid: accountSid });
12
13
async function listCustomFieldMappings() {
14
const customFieldMappings = await client.insights.v3.customFieldMappings.list(
15
{ limit: 20 },
16
);
17
18
customFieldMappings.forEach((c) => console.log(c.id));
19
}
20
21
listCustomFieldMappings();
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": null
18
}
19
],
20
"meta": {
21
"key": "items",
22
"nextToken": null,
23
"pageSize": 50,
24
"previousToken": null
25
}
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/install
2
const twilio = require("twilio"); // Or, for ESM: import twilio from "twilio";
3
4
// Find your Account SID at twilio.com/console
5
// Provision API Keys at twilio.com/console/runtime/api-keys
6
// and set the environment variables. See http://twil.io/secure
7
// For local testing, you can use your Account SID and Auth token
8
const accountSid = process.env.TWILIO_ACCOUNT_SID;
9
const apiKey = process.env.TWILIO_API_KEY;
10
const apiSecret = process.env.TWILIO_API_SECRET;
11
const client = twilio(apiKey, apiSecret, { accountSid: accountSid });
12
13
async function fetchCustomFieldMapping() {
14
const customFieldMapping = await client.insights.v3
15
.customFieldMappings("convinsights_mapping_01m31967b7em08vh1pqgbam3mj")
16
.fetch();
17
18
console.log(customFieldMapping.id);
19
}
20
21
fetchCustomFieldMapping();

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/install
2
const twilio = require("twilio"); // Or, for ESM: import twilio from "twilio";
3
4
// Find your Account SID at twilio.com/console
5
// Provision API Keys at twilio.com/console/runtime/api-keys
6
// and set the environment variables. See http://twil.io/secure
7
// For local testing, you can use your Account SID and Auth token
8
const accountSid = process.env.TWILIO_ACCOUNT_SID;
9
const apiKey = process.env.TWILIO_API_KEY;
10
const apiSecret = process.env.TWILIO_API_SECRET;
11
const client = twilio(apiKey, apiSecret, { accountSid: accountSid });
12
13
async function patchCustomFieldMapping() {
14
const customFieldMapping = await client.insights.v3
15
.customFieldMappings("convinsights_mapping_01m31967b7em08vh1pqgbam3mj")
16
.patch({
17
entityMetadata: {
18
sourceType: "INTELLIGENCE_OPERATOR",
19
intelligenceOperator: {
20
field: "scores.leadScore",
21
},
22
},
23
});
24
25
console.log(customFieldMapping.id);
26
}
27
28
patchCustomFieldMapping();

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 the name, category, description, operatorId, and operatorName, and sending any of them returns 400.
  • The new field follows the same rules as at registration: one or two .-separated segments that resolve to a number or integer property 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 field equal to the current value returns 200 without changing the measure.
  • The optional If-Match header guards against concurrent updates. Set it to the ETag you got from a GET, POST, or previous PATCH response. If the measure has changed since that ETag was issued, the request returns 412 and changes nothing.

Troubleshoot a failed update

troubleshoot-a-failed-update page anchor
ResponseWhat it meansWhat to do
400The 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 propertySend only entityMetadata.intelligenceOperator.field, and confirm the path against the operator's outputSchema
404No measure with that id exists for your accountList your measures to find the correct id
409Another request modified the measure while yours was being appliedFetch the measure again and retry
412The If-Match value doesn't match the measure's current ETagFetch the measure to get its current ETag, then retry
503Conversation Insights couldn't retrieve the operator details it needs to validate the update. The measure wasn't changedRetry after the number of seconds in the Retry-After response header

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 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 field it reads.
  • Names are unique across your account. Registering an existing name returns a 409 response.
  • You can register only numeric fields (number or integer) 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. Existing conversations aren't backfilled. Updating a measure's field applies only to operator results processed afterward.
  • The field path can have, at most, two segments, such as scores.leadScore. Twilio doesn't support array indexing.
  • Conversations where the operator didn't run return null for the measure.

Explore the following resources to learn more: