Building Groups via API
Last updated: April 17, 2026
Overview
The Group Building API lets you programmatically create groups in Unwrap. You provide a natural language description of the feedback theme you want to track, and Unwrap's NLP system will classify matching feedback and build the group in the background.
This is an asynchronous API — the mutation returns immediately with a groupId, and the group build completes in the background (typically 5 – 60 minutes, depending on the size of your feedback dataset). You can monitor progress in the Unwrap UI.
You'll need an API access token and your team ID before making requests. See the Getting Started guide for setup instructions.
Create a Group
Mutation
mutation CreateGroup($teamId: Int!$title: String!$description: String!$parentGroupId: BigInt
$scopeToParentEntries: Boolean
$filterNode: FilterNode
) {
buildGroupAsync(
teamId: $teamId
title: $title
description: $description
parentGroupId: $parentGroupId
scopeToParentEntries: $scopeToParentEntries
filterNode: $filterNode) {
groupId
}}Parameters
Parameter | Type | Required | Description |
|
| Yes | The ID of the team (View) to create the group in. |
|
| Yes | The display title for the group (e.g. |
|
| Yes | A natural language description of the feedback theme. This drives the classifier — the more specific, the better. |
|
| No | If provided, the new group is attached as a child of this group in the taxonomy. Required when using |
|
| No | When |
|
| No | A JSON-encoded filter AST string that restricts the candidate entries considered during the build. Currently only supports metadata (segment) filters. See below. |
Tips for writing a good description
The classifier reads customer feedback — phrase your description the way customers would describe the issue.
Be explicit about what to match and what to exclude. For example:
DO capture customers complaining about price being too high, expensive, or not worth the cost. DO NOT capture customers complaining about billing issues unrelated to pricing.
Response
{"data": {
"buildGroupAsync": {
"groupId": "98765",
}}}groupId— the ID of the new group. Use this to deep-link into the Unwrap UI.
parentGroupId — attaching to the taxonomy
Passing parentGroupId links the new group as a child of an existing group in your taxonomy. The child will appear nested under the parent in the Unwrap UI.
parentGroupId is also a prerequisite for scopeToParentEntries — if you want the child to be scoped to its parent's entries (below), you must pass both.
scopeToParentEntries — creating a filtered child group
Set scopeToParentEntries: true together with a parentGroupId to create a filtered child group. When this flag is set, Unwrap builds the new group as a filter group whose candidate feedback entries are restricted to the entries already in the parent group. The classifier then runs against that scoped set.
This is the right option when you want to break a broad parent theme into sub-themes — e.g., parent group "Pricing complaints" → child group "Annual plan pricing complaints" that only considers entries already classified into "Pricing complaints".
Notes:
scopeToParentEntries: truewithoutparentGroupIdis ignored (it's a no-op).You can combine
scopeToParentEntrieswithfilterNodeto further narrow the scope (parent's entries AND matching the metadata filter).
filterNode — restricting the build with a metadata filter
filterNode takes a JSON-encoded string representing a filter AST. Currently, buildGroupAsync only accepts segment (metadata) field filters — i.e., fields of the form:
Entry.Segment.{segmentGroupId}.value
{segmentGroupId} is the numeric ID of the segment group in Unwrap (you can find this in the Segments admin page, or in the URL when editing a segment). Any other field name will be rejected with:
Filter groups only support segment fields (Entry.Segment.{id}.value). Got: "<fieldName>"
AST shape
A filter node is either a statement (a single comparison) or a collection (a boolean combination of nodes).
Statement:
{"type": "statement","fieldName": "Entry.Segment.42.value","operator": "==","value": "enterprise"}Supported operators for segment values:
OperatorMeaning | |
| Equal |
| Not equal |
| Value is in a JSON array, e.g. |
| Value is not in a JSON array |
| Segment has any value (no |
| Segment has no value (no |
| Greater than or equal (numeric segments only) |
| Less than or equal (numeric segments only) |
Collection:
{"type": "collection","operator": "AND","items": [
{
"type": "statement",
"fieldName": "Entry.Segment.42.value",
"operator": "==",
"value": "enterprise"
},
{
"type": "statement",
"fieldName": "Entry.Segment.17.value",
"operator": "in",
"value": "[\"US\", \"CA\"]"
}]}operator on a collection is either "AND" or "OR", and collections can be nested inside each other.
Important: send filterNode as a stringified JSON
The FilterNode scalar accepts the AST as a JSON string, not a raw object. Always JSON.stringify(...) the AST before putting it in your variables payload.
Simple metadata-filter example
Only consider feedback from enterprise customers when building the group:
{"teamId": 123,"title": "Pricing complaints (enterprise)","description": "DO capture enterprise customers complaining about price being too high, expensive, or not worth the cost. DO NOT capture billing issues unrelated to pricing.","filterNode": "{\"type\":\"statement\",\"fieldName\":\"Entry.Segment.42.value\",\"operator\":\"==\",\"value\":\"enterprise\"}"}curl Example (with parent + scope + filter)
This builds a sub-group under an existing parent group, scoped to the parent's entries, and further filtered to the enterprise segment value:
curl -s -X POST https://data.api.production.unwrap.ai/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-d '{
"query": "mutation CreateGroup($teamId: Int!, $title: String!, $description: String!, $parentGroupId: BigInt, $scopeToParentEntries: Boolean, $filterNode: FilterNode) { buildGroupAsync(teamId: $teamId, title: $title, description: $description, parentGroupId: $parentGroupId, scopeToParentEntries: $scopeToParentEntries, filterNode: $filterNode) { jobId groupId conversationId } }",
"variables": {
"teamId": 123,
"title": "Annual plan pricing complaints (enterprise)",
"description": "DO capture customers complaining about the annual plan being too expensive or not worth the cost. DO NOT capture billing or refund issues.",
"parentGroupId": "55555",
"scopeToParentEntries": true,
"filterNode": "{\"type\":\"statement\",\"fieldName\":\"Entry.Segment.42.value\",\"operator\":\"==\",\"value\":\"enterprise\"}"
}
}'Full Python Example
import json
import os
import requests
ACCESS_TOKEN = os.getenv("UNWRAP_ACCESS_TOKEN")
URL = "https://data.api.production.unwrap.ai/graphql"
HEADERS = {"Authorization": f"Bearer {ACCESS_TOKEN}"}
MUTATION = """
mutation CreateGroup(
$teamId: Int!
$title: String!
$description: String!
$parentGroupId: BigInt
$scopeToParentEntries: Boolean
$filterNode: FilterNode
) {
buildGroupAsync(
teamId: $teamId
title: $title
description: $description
parentGroupId: $parentGroupId
scopeToParentEntries: $scopeToParentEntries
filterNode: $filterNode
) {
jobId
groupId
conversationId
}
}
"""def create_group(
team_id: int,
title: str,
description: str,
parent_group_id: str | None = None,
scope_to_parent_entries: bool = False,
filter_node: dict | None = None,
) -> dict:
"""Kick off an async group build. Returns {jobId, groupId, conversationId}."""
variables = {
"teamId": team_id,
"title": title,
"description": description,
"parentGroupId": parent_group_id,
"scopeToParentEntries": scope_to_parent_entries,
"filterNode": json.dumps(filter_node) if filter_node is not None else None,
}
response = requests.post(URL, json={"query": MUTATION, "variables": variables}, headers=HEADERS)
response.raise_for_status()
data = response.json()
if "errors" in data:
raise Exception(f"GraphQL errors: {data['errors']}")
return data["data"]["buildGroupAsync"]
# Usage: create a sub-group under an existing "Pricing complaints" group,# scoped to its entries and further restricted to the "enterprise" segment.
result = create_group(
team_id=123,
title="Annual plan pricing complaints (enterprise)",
description=(
"DO capture customers complaining about the annual plan being too "
"expensive or not worth the cost. DO NOT capture billing or refund issues."
),
parent_group_id="55555",
scope_to_parent_entries=True,
filter_node={
"type": "statement",
"fieldName": "Entry.Segment.42.value",
"operator": "==",
"value": "enterprise",
},
)
print(f"Group created. ID: {result['groupId']} (job: {result['jobId']})")