Skip to content

Device Groups

Device Groups let you organize managed devices into logical collections for targeted policy assignment, bulk operations, and fleet segmentation. Groups are scoped to an organization and optionally to a site, and they support a parent-child hierarchy for nested grouping.

Breeze supports two group types:

TypeMembershipBest for
StaticDevices are added and removed manually.Fixed collections such as “Executive Laptops” or “Lobby Kiosks”.
DynamicMembership is computed automatically from filter rules. Devices enter and leave the group as their attributes change.Attribute-driven segments such as “Windows Servers with >90% disk” or “Devices offline >7 days”.

Static vs dynamic groups

Static groups

Static groups have a fixed membership list. You add or remove devices explicitly through the API or UI. This is the default group type.

  • Devices are added with addedBy: 'manual'.
  • Devices can be removed individually.
  • No filter conditions are evaluated.

Dynamic groups

Dynamic groups use a filterConditions object to define membership rules. When a dynamic group is created or its filter is updated, the system evaluates the filter against all devices in the organization and automatically adds or removes members.

  • Devices that match the filter are added with addedBy: 'dynamic_rule'.
  • Devices that stop matching are removed automatically — unless they are pinned.
  • You cannot manually add or remove devices from a dynamic group. Use pinning instead.

Creating a group

  1. Choose a name (1—255 characters) and a type (static or dynamic).

  2. Specify the organization the group belongs to. Optionally scope it to a site.

  3. For dynamic groups, define filter conditions (see Dynamic group rules below).

  4. Optionally set a parent group to create a hierarchy. The parent must belong to the same organization.

  5. Submit the request. For dynamic groups, membership evaluation runs asynchronously after creation.

Terminal window
curl -X POST /api/v1/groups \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"orgId": "ORG_UUID",
"name": "Executive Laptops",
"type": "static"
}'

Membership management

Adding devices to a static group

Send an array of device UUIDs. The API verifies that each device exists and belongs to the same organization as the group. Duplicate memberships are silently skipped.

Terminal window
curl -X POST /api/v1/groups/:id/devices \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "deviceIds": ["DEVICE_UUID_1", "DEVICE_UUID_2"] }'

The response reports how many devices were added and how many were skipped (already members):

{
"data": {
"added": 2,
"skipped": 0,
"total": 5
}
}

Removing devices from a static group

Remove a single device by its ID:

Terminal window
curl -X DELETE /api/v1/groups/:id/devices/:deviceId \
-H "Authorization: Bearer $TOKEN"

You can also remove devices in bulk through the alternate endpoint:

Terminal window
curl -X DELETE /api/v1/devices/groups/:id/members \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "deviceIds": ["DEVICE_UUID_1", "DEVICE_UUID_2"] }'

Listing group members

Terminal window
curl /api/v1/groups/:id/devices \
-H "Authorization: Bearer $TOKEN"

Each member record includes:

FieldDescription
deviceIdUUID of the device
hostnameDevice hostname
displayNameOptional display name
statusCurrent device status (online, offline, etc.)
osTypeOperating system type
isPinnedWhether the device is pinned (dynamic groups only)
addedAtTimestamp when the device joined the group
addedByHow the device was added: manual, dynamic_rule, or policy

Dynamic group rules

Dynamic groups use a filterConditions object that follows a recursive AND/OR structure. Each condition targets a specific device field with an operator and value.

Filter condition structure

{
"operator": "AND",
"conditions": [
{ "field": "osType", "operator": "equals", "value": "windows" },
{
"operator": "OR",
"conditions": [
{ "field": "status", "operator": "equals", "value": "offline" },
{ "field": "daysSinceLastSeen", "operator": "greaterThan", "value": 7 }
]
}
]
}

Available filter fields

Fields are organized by category. Each field supports a specific set of operators based on its data type.

FieldLabelTypeExample operators
hostnameHostnamestringequals, contains, startsWith, matches
displayNameDisplay Namestringequals, contains, isNull
statusStatusenumequals, in (values: online, offline, maintenance, decommissioned)
agentVersionAgent Versionstringequals, contains, startsWith
enrolledAtEnrolled Atdatetimebefore, after, withinLast
lastSeenAtLast Seen Atdatetimebefore, after, withinLast
tagsTagsarrayhasAny, hasAll, isEmpty

Operator reference

OperatorApplies toDescription
equals / notEqualsAll typesExact match or negation
greaterThan / greaterThanOrEqualsnumber, dateNumeric or date comparison
lessThan / lessThanOrEqualsnumber, dateNumeric or date comparison
contains / notContainsstring, arrayCase-insensitive substring match (ILIKE)
startsWith / endsWithstringPrefix or suffix match
matchesstringPostgreSQL regex match (~)
in / notInstring, enumValue in or not in an array
hasAny / hasAllarrayArray overlap or superset check
isEmpty / isNotEmptyarrayArray emptiness check
isNull / isNotNullAll typesNull check
before / afterdate, datetimeDate comparison
betweennumber, dateRange check (value: { "from": ..., "to": ... })
withinLast / notWithinLastdate, datetimeRelative time (value: { "amount": 7, "unit": "days" })

Previewing dynamic membership

Before saving filter changes, you can preview which devices would match:

Terminal window
curl -X POST /api/v1/groups/:id/preview?limit=20 \
-H "Authorization: Bearer $TOKEN"

The response includes a total count and a sample of matching devices:

{
"data": {
"totalCount": 47,
"devices": [
{
"id": "...",
"hostname": "SRV-PROD-01",
"displayName": "Production Server 1",
"osType": "linux",
"status": "online",
"lastSeenAt": "2026-02-18T10:30:00.000Z"
}
],
"evaluatedAt": "2026-02-18T10:32:00.000Z"
}
}

The limit query parameter controls the number of sample devices returned (1—100, default 10).

Pinning devices in dynamic groups

Pinning a device to a dynamic group prevents it from being removed when it no longer matches the filter rules. This is useful for devices that must always receive a group’s policies regardless of attribute changes.

Terminal window
# Pin a device
curl -X POST /api/v1/groups/:id/devices/:deviceId/pin \
-H "Authorization: Bearer $TOKEN"
# Unpin a device
curl -X DELETE /api/v1/groups/:id/devices/:deviceId/pin \
-H "Authorization: Bearer $TOKEN"

When a device is unpinned, the system immediately re-evaluates the filter. If the device no longer matches, it is removed from the group.

Group hierarchy

Groups support a parent-child relationship through the parentId field. This lets you organize groups into trees:

All Servers
├── Windows Servers
│ ├── Domain Controllers
│ └── File Servers
└── Linux Servers
└── Web Servers

Rules for hierarchy:

  • A group cannot be its own parent.
  • The parent must belong to the same organization.
  • A group with child groups cannot be deleted. Remove or reassign children first.

Group-level policy assignment

Device groups are a valid assignment target for Configuration Policies. In the policy hierarchy, device group sits between site and device:

Partner (lowest priority)
└── Organization
└── Site
└── Device Group
└── Device (highest priority)

To assign a configuration policy to a device group:

Terminal window
curl -X POST /api/v1/configuration-policies/:policyId/assignments \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"level": "device_group",
"targetId": "GROUP_UUID",
"priority": 10
}'

All devices in the group inherit the policy settings. More specific assignments (at the individual device level) override group-level settings.

Bulk operations on groups

Groups integrate with the deployment system as a target type. When creating a deployment (script execution, patch rollout, software install, or policy push), you can target one or more groups instead of listing individual devices.

The deployment target configuration accepts group IDs:

{
"targetType": "groups",
"targetConfig": {
"type": "groups",
"groupIds": ["GROUP_UUID_1", "GROUP_UUID_2"]
}
}

The deployment system resolves group membership at execution time, so dynamic group changes are reflected automatically.

For quick bulk membership changes on static groups, use the batch endpoints:

Terminal window
# Add multiple devices at once
curl -X POST /api/v1/devices/groups/:id/members \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "deviceIds": ["UUID1", "UUID2", "UUID3"] }'
# Remove multiple devices at once
curl -X DELETE /api/v1/devices/groups/:id/members \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "deviceIds": ["UUID1", "UUID2"] }'

Membership audit log

Every membership change is recorded in the group_membership_log table. You can query the log for a specific group:

Terminal window
curl "/api/v1/groups/:id/membership-log?limit=50&offset=0" \
-H "Authorization: Bearer $TOKEN"

Optional filters:

ParameterDescription
deviceIdFilter to a specific device
actionFilter by added or removed
limitNumber of entries to return (1—500, default 50)
offsetPagination offset (default 0)

Each log entry includes:

FieldDescription
idLog entry UUID
groupIdGroup UUID
deviceIdDevice UUID
hostnameDevice hostname (joined from devices table)
displayNameDevice display name
actionadded or removed
reasonWhy the change occurred: manual, filter_match, filter_unmatch, pinned, or unpinned
createdAtTimestamp of the change

API reference

All group endpoints require authentication and one of the following scopes: organization, partner, or system.

Primary group endpoints (/api/v1/groups or /api/v1/device-groups)

MethodPathDescription
GET/List groups. Query params: siteId, type, parentId, search
POST/Create a group
GET/:idGet a single group
PATCH/:idUpdate a group
DELETE/:idDelete a group (fails if it has child groups)
GET/:id/devicesList devices in a group
POST/:id/devicesAdd devices to a static group
DELETE/:id/devices/:deviceIdRemove a device from a static group
POST/:id/previewPreview dynamic group filter matches. Query: limit
POST/:id/devices/:deviceId/pinPin a device in a dynamic group
DELETE/:id/devices/:deviceId/pinUnpin a device from a dynamic group
GET/:id/membership-logQuery the membership change audit log

Device-scoped group endpoints (/api/v1/devices/groups)

MethodPathDescription
GET/groupsList groups for an org. Query: orgId, page, limit
POST/groupsCreate a group
PATCH/groups/:idUpdate a group
DELETE/groups/:idDelete a group
POST/groups/:id/membersBatch add devices to a group
DELETE/groups/:id/membersBatch remove devices from a group

Database schema

The feature uses three tables:

device_groups

ColumnTypeDescription
iduuid (PK)Auto-generated group ID
org_iduuid (FK)Organization the group belongs to
site_iduuid (FK, nullable)Optional site scope
namevarchar(255)Group name
typeenumstatic or dynamic
rulesjsonbLegacy rules field
filter_conditionsjsonbStructured filter for dynamic groups
filter_fields_usedtext[]Cached list of fields referenced by the filter
parent_iduuid (nullable)Parent group for hierarchy
created_attimestampCreation timestamp
updated_attimestampLast update timestamp

device_group_memberships

ColumnTypeDescription
device_iduuid (FK, PK)Device ID
group_iduuid (FK, PK)Group ID
is_pinnedbooleanWhether the device is pinned (default false)
added_attimestampWhen the device was added
added_byenummanual, dynamic_rule, or policy

group_membership_log

ColumnTypeDescription
iduuid (PK)Log entry ID
group_iduuid (FK)Group ID
device_iduuid (FK)Device ID
actionenumadded or removed
reasonenummanual, filter_match, filter_unmatch, pinned, or unpinned
created_attimestampTimestamp of the change

Troubleshooting

Devices not appearing in a dynamic group

  1. Check filter conditions — Use the POST /api/v1/groups/:id/preview endpoint to verify that the filter matches the expected devices.
  2. Verify organization scope — Dynamic filters only evaluate devices within the group’s orgId. Devices in other organizations are never matched.
  3. Inspect filterFieldsUsed — The system caches which fields a filter references. If the cache is stale, the incremental re-evaluation (updateDeviceMembership) may skip the group because it sees no field overlap with the device change. Updating the group’s filter conditions triggers a full re-evaluation and refreshes the cache.
  4. Check the membership log — Query GET /api/v1/groups/:id/membership-log with the device ID to see if the device was added and then removed.

Cannot add devices to a group

  • Dynamic groups reject manual additions. You will receive: “Cannot manually add devices to a dynamic group”. Use pinning instead or switch the group type to static.
  • Cross-org devices are rejected. All devices must belong to the same organization as the group.

Cannot delete a group

  • Groups with child groups cannot be deleted. The API returns: “Cannot delete group with child groups”. Delete or reassign children first.
  • Deleting a group removes all its membership records automatically.

Pinned devices removed unexpectedly

Pinned devices should never be removed by filter re-evaluation. If a pinned device was removed, check the membership log for a reason of unpinned — someone may have unpinned the device before the filter re-evaluation ran. When a device is unpinned, the system immediately checks whether it still matches the filter and removes it if it does not.

Filter validation errors

When creating or updating a dynamic group with filter conditions, the API validates the filter structure. Common errors:

  • “Unknown field” — The field key does not match any known filter field. Check the Available filter fields tables above. Custom fields must use the custom. prefix.
  • “Operator not valid for field” — The operator is not supported for the field’s data type. For example, greaterThan is not valid for enum fields.
  • “Group must have at least one condition” — A filter condition group cannot be empty.