← Back to guides

Intune Device Sync

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 Intune Microsoft Graph

What you're building

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.

Build order & conventions

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.

Trigger card Kernel / flow control Connector (Graph / Tables) Logic / branching

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.

Field-kind key (inside card tables)

PasteCopy the literal value (click the Copy button) and paste into the field.
PickClick the dropdown and choose the listed option.
DragDrag the named output pill from an earlier card onto this input field.
ToggleFlip the switch.
AddClick + to add a new row / sub-field inside this field group.
0 / 0 complete

Prereq — Register an Entra ID app

P One-time setup in Azure Portal
  1. Azure Portal → Microsoft Entra IDApp registrationsNew registration.
  2. Name: Okta Workflows - Intune Read. Single tenant. Redirect URI: leave blank. Click Register.
  3. On the Overview page, copy these three values to a scratchpad — you'll paste them in Step 1:
    • Application (client) ID
    • Directory (tenant) ID
    • (Client secret you'll create below.)
  4. Left nav → API permissionsAdd a permissionMicrosoft GraphApplication permissions.
  5. Search and add DeviceManagementManagedDevices.Read.All. Click Add permissions, then Grant admin consent for [tenant]. Status column must show the green check.
  6. Left nav → Certificates & secretsNew client secret. Description: Okta Workflows. Expires: 24 months. Click Add.
  7. Immediately copy the Value — it's hidden after you navigate away.
Rotation: Put the secret expiration on your calendar. When Graph starts returning 401 invalid_client, that's almost always an expired secret — not a Workflows bug.

Step 1 — Create the Microsoft Graph connection

1 Workflows → Connections → + New Connection

Top nav: Connections+ New Connection → pick API Connector. Fill every field below.

FieldKindValue
Connection Nickname Paste Microsoft Graph
Authentication Type Pick Client Credentials
Pick 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 body
Microsoft'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.

Handshake failure troubleshooting:
  • 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.
Verify the connection works — Workflows won't tell you. Saving the connection does not probe the token endpoint; the first real validation happens when a card uses it. Confirm both the Entra app and the Graph permission grant before building Step 3:

Option A — curl from your terminal (fastest, decisive):

  1. Token handshake (proves client_id, secret, tenant, scope):
    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.
  2. Graph call (proves 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:

  1. Your folder → + New Flow, name it zz-test-graph-connection.
  2. Add one card: API Connector → Get. (The generic API Connector exposes one card per HTTP verb — Get, Post, Patch, Delete, etc. There's no "Custom API Action" card; that name belongs to Okta's pre-built Microsoft Graph connector, which uses Okta-managed auth and we deliberately bypassed.)
  3. In the card, set Connection to Microsoft Graph.
  4. Set the URL field to the full absolute URL below. Do not use a relative path — the new Client Credentials wizard may not surface a Base URL, in which case Workflows rejects the run with Absolute URI is missing authority segment.
    https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?$top=1
  5. Save, click Run. Output panel: 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: 403DeviceManagementManagedDevices.Read.All permission missing or admin-consent didn't apply — revisit the prereq step.
  6. Delete the test flow when it returns 200.

Step 2 — Create the Intune Devices table

2 Your folder → Tables tab → + New Table

Name: Intune Devices . Add these seven columns in order:

Column nameTypeSource (Graph field)
Device IDTextid
Device NameTextdeviceName
Compliance StateTextcomplianceState
Last Sync DateDatelastSyncDateTime
Enrollment DateDateenrolledDateTime
User Principal NameTextuserPrincipalName
Operating SystemTextoperatingSystem
Compliance values you'll see: compliant, noncompliant, inGracePeriod, configManager, unknown, conflict, error, notAssigned. Keep as text; filter downstream.

Step 3 — Flow 3: Create Intune Device Row

3 Helper flow that writes one row per device

Your folder → + New Flow → name: Create Intune Device Row → type: Helper Flow. This flow has two cards.

C1 Helper Flow Trigger (built-in)
The flow's entry point. We add one input: the device object coming from Flow 2's For Each.
Placement: already present as the first card when you created the flow. Click it and configure its inputs.
Input fieldKindValue
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:
Sub-field nameType
idText
deviceNameText
complianceStateText
lastSyncDateTimeDate & Time
enrolledDateTimeDate & Time
userPrincipalNameText
operatingSystemText
Once declared, these seven sub-fields appear as named pills on the device output of the trigger and can be dragged into the Create Row card below.
Required Toggle On
C2 Create Row Tables connector
Writes one row in Intune Devices, mapped from the incoming device object.
Placement: click + on the line to the right of the Helper Flow trigger → in the card picker search Create Row → select the one listed under Tables.
FieldKindSource / 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 → deviceid
Device Name Drag From trigger → devicedeviceName
Compliance State Drag From trigger → devicecomplianceState
Last Sync Date Drag From trigger → devicelastSyncDateTime
Enrollment Date Drag From trigger → deviceenrolledDateTime
User Principal Name Drag From trigger → deviceuserPrincipalName
Operating System Drag From trigger → deviceoperatingSystem
If the sub-field pills aren't visible on 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.)

Step 4 — Test Flow 3 with one real device

4 Standalone smoke test before wiring up pagination

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.

You'll need an Intune device ID first. Open intune.microsoft.comDevices 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.
C1 Manual Flow Trigger (built-in)
Lets you click Save & Run to fire the flow on demand. No inputs to configure.
Placement: already present as the first card. Nothing to set.
C2 Get API Connector
Calls Graph's get-by-ID endpoint. Returns the single device object directly — no value array wrapper, so we hand body straight to Flow 3.
Placement: click + to the right of the Manual Flow trigger → search API Connector → pick Get. Click the card's gear icon → Connection → pick 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.

FieldKindSource / 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.
C3 Call Flow Flow control (kernel)
Hands the single-device response to Create Intune Device Row — same shape Flow 2's For Each will pass in production.
Placement: click + after the API Connector Get card → search Call Flow (not Call Flow Async) → pick the one under Control.
FieldKindSource / 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.

Expected 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.
Want to query by name or UPN instead of ID? Swap C2's URL for a filter query, then add a Get Item from List card (List kernel, index 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.
Run failed?
  • 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).
  • C3 throws a "field type mismatch" → one of the Date columns (Last Sync Date, Enrollment Date) was created as Text in Step 2, or vice versa. Open the table and fix the column type.
  • Row appears but some columns are blank → one of the seven drag wirings on Flow 3's C2 is unmapped. Open Flow 3, check every row field has a pill assigned.

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.

Step 5 — Flow 2: Fetch Intune Devices Page

5 Recursive paginator that calls Graph and iterates devices

+ New Flow → name: Fetch Intune Devices Page → type: Helper Flow. Five cards total.

C1 Helper Flow Trigger (built-in)
Entry point. Accepts a Graph URL to fetch — either the initial devices URL on first call, or an @odata.nextLink on recursion.
Placement: first card (already present). Add one input.
Input fieldKindValue
Input name Paste pageUrl
Input type Pick Text
Required Toggle On
C2 Get API Connector
Calls Microsoft Graph at pageUrl. Returns a body with a value array (one page of devices) and optionally @odata.nextLink (next page URL).
Placement: click + to the right of the Helper Flow trigger → search API Connector → pick Get. Click the card's gear icon → Connection → pick 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.

FieldKindSource / 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.

C3 For Each List (kernel)
Loops over body.value (the page's devices) and calls Flow 3 once per device.
Placement: click + after the API Connector Get card → search For Each → pick the one under List.
FieldKindSource / 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.
C4 Continue If Flow control (kernel)
Halts this flow execution if there's no next page. Lets it continue on to the recursion card only when Graph returned an @odata.nextLink.
Placement: click + after For Each → search Continue If → pick the one under Control.

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.

FieldKindSource / 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.

C5 Call Flow Flow control (kernel)
Recurses into this same flow with the @odata.nextLink as the new pageUrl, fetching the next page.
Placement: click + after Continue If → search Call Flow (not Call Flow Async) → pick the one under Control.
FieldKindSource / 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.
Recursion safety: Workflows caps synchronous Call Flow depth at ~1000 levels. At $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.

Step 6 — Flow 1: Intune Device Sync (scheduled main)

6 Scheduled flow that kicks the whole pipeline off daily

+ New Flow → name: Intune Device Sync → type: Scheduled Flow. Four cards.

C1 Scheduled Trigger Trigger (built-in)
Fires the flow on a schedule. We'll turn it on at the very end.
Placement: already present. Click the Schedule tab on the card to set the cadence.
FieldKindValue
Frequency Pick Daily
Time Paste 02:00
Time zone Pick America/New_York (or your preferred TZ)
C2 Clear Table Tables connector
Wipes Intune Devices so each run is a clean snapshot. (If you want history, swap this for a Search Rows / upsert later.)
Placement: click + after the Scheduled Trigger → search Clear Table → pick the one under Tables.
FieldKindValue
Table Pick Intune Devices
C3 Assign Flow control (kernel)
Stores the initial Graph URL in a variable, so downstream cards have a clean named pill to drag.
Placement: click + after Clear Table → search Assign → pick the one under Control. Click the card → + Add a variable.
FieldKindValue
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
C4 Call Flow Flow control (kernel)
Kicks off the paginator helper with the initial Graph URL.
Placement: click + after Assign → search Call Flow → pick the one under Control.
FieldKindSource / 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.

Step 7 — Test end-to-end

7 First manual run, then verify rows
  1. Confirm Create Intune Device Row (Flow 3) toggle is ON.
  2. Confirm Fetch Intune Devices Page (Flow 2) toggle is ON.
  3. Leave Intune Device Sync (Flow 1) OFF. Open it and click Save & run to trigger one manual execution.
  4. In Flow 1's execution history, click the run and confirm each card ran green: Clear Table, Assign, Call Flow.
  5. Open Flow 2's execution history — you'll see one execution per page of devices. Each should show a 200 on the API Connector Get and N For Each iterations.
  6. Open the Intune Devices table — you should have one row per device currently in Intune.
No rows appeared? Open the API Connector Get card's execution → Output. Common failures:
  • 401 → connection handshake failed; re-verify tenant ID, client secret, and admin consent.
  • 403 ForbiddenDeviceManagementManagedDevices.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.
After the first successful run, go back to Flow 2's Continue If card (C4) and Flow 3's Create Row card (C2). The pills for @odata.nextLink and the seven device fields should now be visible — finish any wiring you deferred earlier.

Step 8 — Turn on the schedule

8 Flip Flow 1 to ON

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).

Tuning & known edges

Extending this pattern

The paginator-helper architecture generalizes to any paged Microsoft Graph endpoint. To pivot this workflow to a different data source, change two things:

  1. The initial 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.
  2. The column set on the table and the field mapping in the Create Row helper.

Useful Intune-adjacent endpoints that work identically. Prefix each path below with https://graph.microsoft.com/v1.0 when pasting into the Assign card:

GoalPath (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

Related