Build an Okta Workflow that pulls every Microsoft Intune-managed device into a Workflows table on a daily schedule. Captures device name, compliance state, last check-in, and enrollment date for audit, reporting, or downstream automation.
Okta Workflows has no native Microsoft Intune connector. Querying Intune means calling Microsoft Graph at /deviceManagement/managedDevices with an app-only OAuth token. This guide walks you through building the whole pipeline in the Workflows UI — starting from an empty folder.
Three flows, one table, one connection:
Once running you get a table you can join against Okta users, filter for noncompliant devices, drive group-membership decisions, or pipe into a BI tool.
We build bottom-up: Flow 3 first, then Flow 2, then Flow 1. That way, every Call Flow / For Each card has a real target flow to pick when you configure it — no half-wired state.
Inside each step, every card gets its own block with four things: which card to pick, what it does, where it goes in the flow, and exactly what to paste / drag into each field.
| Paste | Copy the literal value (click the Copy button) and paste into the field. | |
| Pick | Click the dropdown and choose the listed option. | |
| Drag | Drag the named output pill from an earlier card onto this input field. | |
| Toggle | Flip the switch. | |
| Add | Click + to add a new row / sub-field inside this field group. | |
Okta Workflows - Intune Read. Single tenant. Redirect URI: leave blank. Click Register.DeviceManagementManagedDevices.Read.All. Click Add permissions, then Grant admin consent for [tenant]. Status column must show the green check.Okta Workflows. Expires: 24 months. Click Add.401 invalid_client, that's almost always an expired secret — not a Workflows bug.
Top nav: Connections → + New Connection → pick API Connector. Fill every field below.
| Field | Kind | Value |
|---|---|---|
| Connection Nickname | Paste | Microsoft Graph
|
| Authentication Type | Pick | Client CredentialsPick this directly — it's its own top-level option, not nested under "OAuth". There is no separate Grant Type field. |
| Client ID | Paste | Your Application (client) ID from the prereq step. |
| Client Secret | Paste | The Secret Value (not Secret ID) from the prereq step. |
| Access Token Path | Paste | https://login.microsoftonline.com/YOUR-TENANT-ID/oauth2/v2.0/token
Full URL, not a relative path. Replace YOUR-TENANT-ID with the Directory (tenant) ID from the prereq step. |
| Scope | Paste | https://graph.microsoft.com/.default
|
| Client Authentication Type | Pick | Send as basic auth bodyMicrosoft's documented client_credentials flow expects client_id and client_secret in the form-encoded body. The header variant also works against Entra ID, but body matches Microsoft's reference and is the safer default. |
| Base URL | Paste (if shown) | https://graph.microsoft.com/v1.0
The new Client Credentials wizard may omit this field. That's fine — every URL in the verification step and Steps 4–7 is a full absolute https://graph.microsoft.com/... URL, so card-side concatenation is never required. |
Click Create. Workflows performs the token handshake.
AADSTS700016 / invalid client → you pasted the app's Object ID instead of Application (client) ID.AADSTS7000215 / invalid client secret → you pasted the Secret ID instead of the Secret Value, or it was truncated.AADSTS50011 / redirect URI mismatch → wrong Authentication Type; make sure you picked Client Credentials, not OAuth. The OAuth option does the interactive auth-code flow and needs a redirect URI you didn't register.AADSTS7000218 / "client_assertion or client_secret" required → flip Client Authentication Type to the other option (header ↔ body) and recreate the connection.AADSTS900023 / tenant not found → tenant ID wrong or still placeholder.Option A — curl from your terminal (fastest, decisive):
curl -X POST "https://login.microsoftonline.com/YOUR-TENANT-ID/oauth2/v2.0/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "client_id=YOUR-CLIENT-ID" \
--data-urlencode "client_secret=YOUR-SECRET-VALUE" \
--data-urlencode "scope=https://graph.microsoft.com/.default" \
--data-urlencode "grant_type=client_credentials"
Pass = JSON with access_token. Fail = JSON with error + error_description — the description tells you exactly which field is wrong.
DeviceManagementManagedDevices.Read.All consent landed):
curl "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?\$top=1" \
-H "Authorization: Bearer PASTE_TOKEN_FROM_STEP_1"
Pass = {"value":[{...}]} with a device. 403 InsufficientPrivileges = admin-consent didn't fully apply — go back to the prereq and re-click Grant admin consent.
Option B — one-card test flow inside Workflows:
zz-test-graph-connection.Microsoft Graph.Absolute URI is missing authority segment.
https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?$top=1
statusCode: 200 + body with a value array = connection works. Common failures:
Absolute URI is missing authority segment → you pasted a relative path. Use the full https://... URL above.statusCode: 401 → token handshake failed. Re-check Client ID, Client Secret Value, and the tenant ID inside the Access Token Path.statusCode: 403 → DeviceManagementManagedDevices.Read.All permission missing or admin-consent didn't apply — revisit the prereq step.Name: Intune Devices . Add these seven columns in order:
| Column name | Type | Source (Graph field) |
|---|---|---|
Device ID | Text | id |
Device Name | Text | deviceName |
Compliance State | Text | complianceState |
Last Sync Date | Date | lastSyncDateTime |
Enrollment Date | Date | enrolledDateTime |
User Principal Name | Text | userPrincipalName |
Operating System | Text | operatingSystem |
compliant, noncompliant, inGracePeriod, configManager, unknown, conflict, error, notAssigned. Keep as text; filter downstream.
Your folder → + New Flow → name: Create Intune Device Row → type: Helper Flow. This flow has two cards.
| Input field | Kind | Value | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Input name | Paste | device
| ||||||||||||||||
| Input type | Pick | Object | ||||||||||||||||
| Object sub-fields | Add | An Object input lets you declare its sub-fields directly on the trigger card (Workflows does not auto-infer them from runtime data). Add these seven sub-fields by name and type:
device output of the trigger and can be dragged into the Create Row card below. | ||||||||||||||||
| Required | Toggle | On |
Intune Devices, mapped from the incoming device object.| Field | Kind | Source / value |
|---|---|---|
| Table | Pick | Intune Devices (from Step 2) |
Row fields — all seven are drag-wired from the device input pill | ||
| Device ID | Drag | From trigger → device → id |
| Device Name | Drag | From trigger → device → deviceName |
| Compliance State | Drag | From trigger → device → complianceState |
| Last Sync Date | Drag | From trigger → device → lastSyncDateTime |
| Enrollment Date | Drag | From trigger → device → enrolledDateTime |
| User Principal Name | Drag | From trigger → device → userPrincipalName |
| Operating System | Drag | From trigger → device → operatingSystem |
device: open the Helper Flow trigger (C1) and confirm all seven sub-fields are declared with the correct names and types. Workflows surfaces sub-field pills only because they're explicitly declared on the trigger — it does not infer them from runtime data, so the declaration is the only thing that makes them appear.
Save the flow. Toggle ON. (Helpers must be on before their callers fire.)
Before building the pagination chain, fire Flow 3 once with a real device payload. This catches any column/type mismatches in the Intune Devices table early and confirms the seven sub-field declarations on Flow 3's trigger map cleanly to the Graph response shape. (Sub-field pills are available because they were declared on the Helper Flow trigger in Step 3 — not because Workflows discovered them from runtime data.)
+ New Flow → name: Test Intune Device Row → type: Flow (not Helper — we want to run it manually). Three cards.
intune.microsoft.com → Devices in the left nav → All devices. Click any row to open that device's blade. The browser URL bar now contains a 36-character GUID (looks like abcd1234-5678-90ab-cdef-1234567890ab) — that's the Intune device ID you'll paste into C2's URL field below.
value array wrapper, so we hand body straight to Flow 3.Microsoft Graph.A Get card has three input fields: URL, Headers, Query. (Connection is set via the gear icon, covered in the placement step above.) Output is status code, headers, body.
| Field | Kind | Source / value |
|---|---|---|
| URL | Paste | https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/REPLACE_WITH_DEVICE_ID?$select=id,deviceName,complianceState,lastSyncDateTime,enrolledDateTime,userPrincipalName,operatingSystem
Replace REPLACE_WITH_DEVICE_ID with the GUID you grabbed above. |
| Headers | — | Leave empty. Workflows implicitly sets Content-Type: application/json on every API Connector card, and Microsoft Graph returns JSON by default — no manual Accept header is needed for this endpoint. |
| Query | — | Leave empty. The $select is already part of the URL above; no separate query parameters needed. |
Create Intune Device Row — same shape Flow 2's For Each will pass in production.| Field | Kind | Source / value |
|---|---|---|
| Flow | Pick | Create Intune Device Row (Flow 3, Step 3) |
| device (appears after picking Flow) | Drag | From API Connector Get → drag the entire body pill onto device. Don't drill into sub-fields — the helper's input is the whole object. |
Save the flow. Make sure Create Intune Device Row (Flow 3) is toggled ON, then click Save & Run on this test flow's trigger card. Open the Intune Devices table — one row should appear, populated from your real device.
body shape from C2 (so you can sanity-check the run output):
{
"id": "abcd1234-5678-90ab-cdef-1234567890ab",
"deviceName": "LAPTOP-ABC123",
"complianceState": "compliant",
"lastSyncDateTime": "2026-04-29T10:15:32Z",
"enrolledDateTime": "2025-06-12T08:00:00Z",
"userPrincipalName": "alice@contoso.com",
"operatingSystem": "Windows"
}
Graph returns more fields than these; Flow 3 only consumes the seven you defined as table columns.
0) between C2 and C3:
https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?$filter=deviceName eq 'LAPTOP-ABC123'&$select=id,deviceName,complianceState,lastSyncDateTime,enrolledDateTime,userPrincipalName,operatingSystem
C3's device input then comes from the Get Item card's output pill (not C2's body). Filter alternatives: userPrincipalName eq 'alice@contoso.com', complianceState eq 'noncompliant'. The by-ID path is simpler because Graph returns the object directly with no array unwrapping.
404 on C2 → the device ID is wrong, or the device exists in Entra ID but not in Intune (only enrolled devices appear under managedDevices).403 on C2 → DeviceManagementManagedDevices.Read.All is missing, or admin consent wasn't granted (revisit Step 1).Last Sync Date, Enrollment Date) was created as Text in Step 2, or vice versa. Open the table and fix the column type.Leave this test flow in the folder. It's handy any time you change Flow 3's mapping or the table schema and want a fast, single-device regression check without re-running the full nightly sync.
+ New Flow → name: Fetch Intune Devices Page → type: Helper Flow. Five cards total.
@odata.nextLink on recursion.| Input field | Kind | Value |
|---|---|---|
| Input name | Paste | pageUrl
|
| Input type | Pick | Text |
| Required | Toggle | On |
pageUrl. Returns a body with a value array (one page of devices) and optionally @odata.nextLink (next page URL).Microsoft Graph.A Get card has three input fields: URL, Headers, Query. (Connection is set via the gear icon, covered in the placement step above.) The output is statusCode, headers, and a body Section — the body Section is where you declare which response fields you want to surface as named output pills downstream.
| Field | Kind | Source / value |
|---|---|---|
| URL | Drag | From the Helper Flow trigger output, drag the pageUrl pill into this field. |
| Headers | — | Leave empty. Workflows implicitly sets Content-Type: application/json on every API Connector card, and Microsoft Graph returns JSON by default — no manual Accept header is needed for this endpoint. |
| Query | — | Leave empty. The URL already contains $select and $top=100; Graph's @odata.nextLink carries query state on recursion. |
| Body output paths — declare what to surface from the response | ||
| body → output 1 | Add | In the body Section of the card's output configuration, add an output. Name it value
, set its type to List, and set its path to value
. This exposes the array of devices as a draggable pill named value on the card's output, ready to feed the For Each below. |
| body → output 2 | Add | Add a second output. Name it nextLink
, set its type to Text, and set its path to @odata.nextLink
. This exposes Graph's pagination cursor as a draggable pill named nextLink on the card's output, used by Continue If (C4) and Call Flow (C5) below. |
Why declare these: the body Section is where you tell Workflows which response fields you actually want exposed downstream. Without declared output paths, the body is one opaque object pin and downstream cards can't drag specific sub-fields out of it.
body.value (the page's devices) and calls Flow 3 once per device.| Field | Kind | Source / value |
|---|---|---|
| List | Drag | From the API Connector Get card's output, drag the value pill (the named output you declared in C2 with path value) onto this input. |
| Flow | Pick | Create Intune Device Row (Flow 3, Step 3) |
| device (appears after picking Flow) | Drag | Drag the For Each card's loop pill (the current item) onto the device input. |
@odata.nextLink.A Continue If card has three configurable inputs — value a, comparison, value b — plus an optional message. The is empty and is not empty operators are unary: only value a is evaluated; value b is ignored.
| Field | Kind | Source / value |
|---|---|---|
| value a | Drag | From the API Connector Get card's output, drag the nextLink pill (the named output you declared in C2 with path @odata.nextLink) into this field. Set its type to Text. |
| comparison | Pick | is not empty |
| value b | — | Leave empty. is not empty is a unary operator and ignores this field. |
| message | — | Optional. Leave empty for production; you can paste something like No more pages while debugging so the run history shows why the flow halted. |
The flow halts when nextLink is missing/null/empty and continues to the recursion card only when Graph gave us another page URL.
@odata.nextLink as the new pageUrl, fetching the next page.| Field | Kind | Source / value |
|---|---|---|
| Flow | Pick | Fetch Intune Devices Page (this flow — it calls itself recursively) |
| pageUrl (appears after picking Flow) | Drag | From the API Connector Get card's output, drag the nextLink pill (the named output declared in C2). Graph's nextLink is already a full URL — no concatenation needed. |
$top=100 that's 100k devices before you hit the ceiling. For very large tenants lower $top and expect more pages, or swap in Call Flow Async (loses pagination ordering but lifts the depth limit).
Save the flow. Toggle ON.
+ New Flow → name: Intune Device Sync → type: Scheduled Flow. Four cards.
| Field | Kind | Value |
|---|---|---|
| Frequency | Pick | Daily |
| Time | Paste | 02:00 |
| Time zone | Pick | America/New_York (or your preferred TZ) |
Intune Devices so each run is a clean snapshot. (If you want history, swap this for a Search Rows / upsert later.)| Field | Kind | Value |
|---|---|---|
| Table | Pick | Intune Devices |
| Field | Kind | Value |
|---|---|---|
| Name | Paste | graphUrl
|
| Type | Pick | Text |
| Value | Paste | https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?$select=id,deviceName,complianceState,lastSyncDateTime,enrolledDateTime,userPrincipalName,operatingSystem&$top=100
|
| Field | Kind | Source / value |
|---|---|---|
| Flow | Pick | Fetch Intune Devices Page (Flow 2, Step 5) |
| pageUrl (appears after picking Flow) | Drag | From the Assign card → drag the graphUrl output pill into pageUrl. |
Save the flow. Leave it OFF for now. We'll run it manually once in Step 7 before enabling the schedule.
Intune Devices table — you should have one row per device currently in Intune.401 → connection handshake failed; re-verify tenant ID, client secret, and admin consent.403 Forbidden → DeviceManagementManagedDevices.Read.All missing or not consented.200 with empty body.value → your tenant genuinely has no enrolled devices, or the Entra app doesn't have visibility because of conditional access restrictions on app-only tokens.@odata.nextLink and the seven device fields should now be visible — finish any wiring you deferred earlier.
Once the manual run in Step 7 writes rows cleanly, open Intune Device Sync → flip the top-right toggle to ON. The next fire is at your configured time (default: 02:00 America/New_York daily).
complianceState can return configManager. Treat these as "trust the on-prem side" or filter them out before compliance alerting.https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/delta and persist the @odata.deltaLink in a scratch table. Subsequent runs only return changed devices.$top=100, a 10k-device tenant is 100 calls — fits comfortably. At $top=50 you'll hit the limit; add a Wait For card in the paginator (e.g., 250ms) if you lower the page size.$select. Trim further if you don't need UPN or OS.graph.microsoft.com with graph.microsoft.us (GCC-High) or microsoftgraph.chinacloudapi.cn (China 21V). Tenant and token URLs also differ — check Microsoft's sovereign cloud matrix.The paginator-helper architecture generalizes to any paged Microsoft Graph endpoint. To pivot this workflow to a different data source, change two things:
graphUrl in Flow 1's Assign card. Use the full absolute URL with the https://graph.microsoft.com/v1.0 prefix — e.g. https://graph.microsoft.com/v1.0/users or https://graph.microsoft.com/v1.0/groups.Useful Intune-adjacent endpoints that work identically. Prefix each path below with https://graph.microsoft.com/v1.0 when pasting into the Assign card:
| Goal | Path (prefix with https://graph.microsoft.com/v1.0) |
|---|---|
| Devices for one user | /users/{upn}/managedDevices |
| Noncompliant only | /deviceManagement/managedDevices?$filter=complianceState eq 'noncompliant' |
| Apple devices only | /deviceManagement/managedDevices?$filter=operatingSystem eq 'iOS' or operatingSystem eq 'macOS' |
| Stale devices (> 30 days) | /deviceManagement/managedDevices?$filter=lastSyncDateTime lt 2026-03-18T00:00:00Z |
| Apps deployed to devices | /deviceAppManagement/mobileApps |
| Configuration profiles | /deviceManagement/deviceConfigurations |