A PowerShell automation engine that automatically keeps Entra ID group membership in sync with live Intune device inventory — driven by YAML rules, no code required. Run on a schedule, on demand, or triggered instantly by a CI/CD pipeline whenever rules change.
- What is the Intune Adaptive Engine?
- Prerequisites
- Quick Start — One-Command Deployment
- Quick Start — Local Testing
- Rule Reference
- Engine Configuration Reference
- CI/CD Pipeline Integration
- Troubleshooting
Entra ID Dynamic Groups are the native tool for rule-based group membership, but they come with three meaningful limitations:
-
Licensing gate. Dynamic Groups require Entra ID P1 or P2 licensing, which many organisations do not have — or do not want to pay for just to get device targeting.
-
Wrong data source. Dynamic Group rules evaluate against the Entra device object, not the Intune managed device object. These are two separate entities in Microsoft's data model. Intune-specific properties —
complianceState,lastSyncDateTime,model,isEncrypted,deviceCategoryDisplayName— live on the Intune managed device record and are not available to dynamic group rules. -
No app-based targeting. There is no native way to build a group based on what applications are actually installed on a device, regardless of how they got there.
Assignment Filters are a different tool entirely. They act as per-policy gates evaluated at device check-in — they do not create or manage groups, cannot be queried for membership, and cannot be used outside of Intune.
The Intune Adaptive Engine bridges these gaps. It reads YAML rules you define, queries Intune device inventory from the Microsoft Graph API, evaluates your conditions, and reconciles Entra ID group membership — without agents, without P1/P2 licensing, and using properties that dynamic groups cannot touch.
YAML Rules Graph API (Intune) Entra ID Groups
────────── → ──────────────────── → ─────────────────
conditions managed device inventory membership synced
per-rule detectedApps inventory add / remove
groupId ref current group members idempotent diff
Each engine run:
- Authenticates to Microsoft Graph (Managed Identity or App Registration)
- Fetches all managed device records from Intune across all platforms
- Fetches detected application inventory
- Loads all enabled YAML rule files from the configured rules folder
- For rules that reference
detectedApps, pre-fetches per-app device lists (smart platform/name filter avoids unnecessary API calls) - Evaluates each rule's conditions against every device
- Diffs the evaluated membership against the current Entra group membership
- Issues the minimum set of add/remove API calls to bring the group in sync
- Emits a structured run summary
A failure in one rule does not abort the run. Only unrecoverable errors (authentication, config, missing module) stop the engine entirely.
| Capability | Detail |
|---|---|
| 20+ device properties | All Intune-native Graph properties including osVersion, complianceState, model, lastSyncDateTime, isEncrypted, and more |
| detectedApps | Target groups based on applications detected in Intune inventory — device or user groups |
| conditionGroups | Mixed AND/OR logic across groups of conditions — combine multiple criteria sets with independent logic per group |
| -WhatIf mode | Engine-level dry run: computes what would change, logs it, touches nothing |
| dryRun per rule | Same as WhatIf but scoped to individual rules in the YAML file |
| Auto group creation | Leave groupId blank — the engine creates the group and writes the ID back |
| Version-controlled rules | Plain YAML files, suitable for Git, pull request review, and CI/CD triggers |
| Platform filtering | Per-rule platform field restricts evaluation to macOS, Windows, iOS, Android, or chromeOS |
-
Azure subscription — for the Automation Account and supporting resources
-
Azure DevOps organisation and project — repository for rule files and CI/CD pipeline
-
Microsoft Intune tenant — devices must be enrolled and checking in
-
PowerShell 7.2 or later — required locally and in the Azure Automation runtime
-
powershell-yamlmodule — the engine auto-installs it if missing, or install manually:Install-Module powershell-yaml -Scope CurrentUser
-
Microsoft Graph API permissions (Application, assigned to the Managed Identity):
Permission Purpose DeviceManagementManagedDevices.Read.AllRead device inventory from Intune Group.ReadWrite.AllRead group metadata and create Security groups GroupMember.ReadWrite.AllAdd and remove members from existing groups -
Administrator access — Contributor on the Azure subscription and Global Administrator (or Privileged Role Administrator) in the Entra tenant to assign application permissions to the Managed Identity
For a fresh deployment, the interactive setup script handles everything from zero to a running engine in a single session:
cd setup
.\Invoke-IAEDeployment.ps1The script walks through eight phases:
| Phase | What it does |
|---|---|
| 1 | Collects all inputs interactively (nothing prompted mid-execution) |
| 2 | Creates the resource group and Automation Account with system-assigned Managed Identity |
| 3 | Assigns the three required Graph API permissions |
| 4 | Guides you through the DevOps manual step, then adds the MI to project Contributors |
| 5 | Creates the four Automation Variables (IAE_DevOpsOrgUrl, IAE_DevOpsProject, IAE_DevOpsRepo, IAE_DevOpsBranch) |
| 6 | Zips and uploads GraphCollector, RuleEngine, and GroupSync modules; installs powershell-yaml from gallery |
| 7 | Imports and publishes the Orchestrator runbook directly to the Automation Account |
| 8 | Prints a summary with portal links and next steps |
After the script completes — two manual steps remain:
Step 1 — Set the runbook runtime to PowerShell 7.2:
- Portal → Automation Account → Process Automation → Runbooks → Orchestrator
- Click Edit → change Runtime version from
5.1to7.2→ click Publish
Step 2 — Push files to Azure DevOps:
cd intune-adaptive-engine
git init
git remote add origin https://dev.azure.com/{yourorg}/{yourproject}/_git/{yourrepo}
git add IntuneAE/Orchestrator.ps1 IntuneAE/config/ IntuneAE/rules/
git commit -m "Initial IAE deployment"
git branch -M main
git push origin mainThe engine fetches engine.yaml and rule files from your DevOps repo at runtime. Without this step the first run fails with a 404 error.
For a step-by-step manual walkthrough, see docs/DEPLOYMENT.md.
Local testing uses an App Registration with a client secret. This is the recommended path for development and validation before deploying to production.
- Go to Entra admin centre > App registrations > New registration. Name it and click Register.
- Note the Application (client) ID and Directory (tenant) ID.
- Go to API permissions > Add > Microsoft Graph > Application permissions. Add the three permissions from the table in Section 2.
- Click Grant admin consent.
- Go to Certificates & secrets > New client secret. Copy the value immediately.
Create IntuneAE/rules/my-first-rule.yaml:
name: Non-Compliant macOS Devices
description: All macOS devices currently non-compliant in Intune
groupId: # leave blank — engine creates the group on first run
groupType: device
enabled: true
dryRun: true # start with dry-run
logic: AND
platform: macOS
conditions:
- property: complianceState
operator: equals
value: noncompliantpwsh -File IntuneAE/src/Orchestrator.ps1 `
-AuthMode AppRegistration `
-TenantId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' `
-ClientId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' `
-ClientSecret 'your-client-secret-value' `
-WhatIf `
-VerboseCheck the output for [WHAT-IF] lines. When satisfied, set dryRun: false in the rule
file and remove -WhatIf from the command.
To deep dive further in the rule references, please see 'IAE-Rule-Cheatsheet' in the Docs folder. Or alternatively use the rule-builder that's in the Docs folder as well.
name: My Rule
groupId:
conditions:
- property: operatingSystem
operator: equals
value: macOSname: Legacy Intel Macs # required; used as group display name
description: Human-readable summary # optional
groupId: xxxxxxxx-xxxx-xxxx-xxxx-xxxx # populated by engine on first run
groupType: device # device (default) | user
enabled: true # default: true
dryRun: false # default: false
logic: AND # AND (default) | OR
platform: macOS # macOS | windows | iOS | android | chromeOS
conditions:
- property: model
operator: contains
value: Intel
- property: osVersion
operator: lessThan
value: "14.0"| Operator | Applies to |
|---|---|
equals / notEquals |
Any string or boolean property |
contains / notContains |
String properties |
startsWith |
String properties |
lessThan / greaterThan |
Semantic version (14.3.1) or numeric with optional unit suffix (10 GB) |
olderThanDays / newerThanDays |
DateTime properties (lastSyncDateTime, enrolledDateTime) |
isTrue / isFalse |
Boolean properties (isEncrypted, isSupervised) |
in |
Matches if property value is in a YAML list |
conditions:
- property: detectedApps
operator: contains
value: Google Chrome
version: # optional version sub-condition
operator: lessThan
value: "120.0"logic: OR # combine groups with OR at the top level
conditionGroups:
- logic: AND # all conditions in this group must match
conditions:
- property: model
operator: contains
value: Intel
- property: osVersion
operator: lessThan
value: "14.0"
- logic: AND
conditions:
- property: complianceState
operator: equals
value: noncompliantIntuneAE/config/engine.yaml:
schedule: 60 # run interval in minutes (informational — set in Automation Schedule)
rulesSource:
type: local # only 'local' is supported in this release
path: rules/ # relative to IntuneAE/ project root
auditLog: tableStorage # tableStorage | logAnalytics
autoGroupCreation:
enabled: true # create Entra groups for rules with blank groupId
groupPrefix: "IAE - " # prepended to rule name for group display nameazure-pipelines.yml at the repository root triggers the Orchestrator runbook automatically
when rule files or engine.yaml change:
- Pull request → main: runs in WhatIf mode — rule authors see membership previews before the PR is merged
- Push to main: runs in live mode — group membership is updated right after merge
Set in Azure DevOps → Pipelines → Edit → Variables:
| Variable | Description |
|---|---|
IAE_ResourceGroup |
Azure resource group containing the Automation Account |
IAE_AutomationAccount |
Automation Account name |
IAE_ServiceConnection |
Azure DevOps service connection with Automation Operator rights |
Modules were uploaded for the wrong runtime version, or import is still in progress. Go to Automation Account > Modules and confirm all three show Available with Runtime version 7.2. If any show a different version or error, delete and re-upload.
The runbook runtime is PowerShell 5.1 instead of 7.2. Go to Process Automation → Runbooks → Orchestrator → Edit, change Runtime version to 7.2, and click Publish.
A required Graph API permission is missing. Re-run:
.\setup\Grant-GraphPermissions.ps1 -ManagedIdentityObjectId "your-object-id"The Managed Identity does not yet have Code write access to the DevOps project. Re-run:
.\setup\Add-ManagedIdentityToDevOps.ps1 `
-ManagedIdentityObjectId "your-object-id" `
-DevOpsOrgUrl "https://dev.azure.com/yourorg" `
-DevOpsProject "your-project"Then manually add groupId: <id> to the affected rule file and delete any duplicate Entra groups.
intune-adaptive-engine/
├── README.md
├── azure-pipelines.yml
├── LICENSE
├── IntuneAE/
│ ├── Orchestrator.ps1 ← deployable runbook (uploaded by Invoke-IAEDeployment.ps1)
│ ├── src/
│ │ ├── GraphCollector.psm1 ← Graph API authentication and data retrieval
│ │ ├── GraphCollector.psd1
│ │ ├── RuleEngine.psm1 ← YAML rule loader and condition evaluator
│ │ ├── RuleEngine.psd1
│ │ ├── GroupSync.psm1 ← Entra group membership reconciliation
│ │ ├── GroupSync.psd1
│ ├── config/
│ │ └── engine.yaml
│ ├── rules/
│ │ └── examples/
│ │ ├── legacy-intel-macs.yaml
│ │ ├── google-chrome-detected-devices.yaml
│ │ └── slack-detected-users.yaml
├── setup/
│ ├── Invoke-IAEDeployment.ps1 ← single-script onboarding (recommended)
│ ├── Grant-GraphPermissions.ps1
│ └── Add-ManagedIdentityToDevOps.ps1
└── docs/
├── DEPLOYMENT.md ← step-by-step manual deployment guide
└── rule-builder.html ← browser-based YAML rule builder