If your script is still lean on MS Online or Azure AD, they're already legacy.
And if you think that doesn't apply to you,
oh boy, you're exactly who I'm talking to.
The cloud moved on.
Your modules didn't.
Modules break on Linux runners, containers, CI/CD.
Rest doesn't.
PowerShell isn't going away.
The modules are, we're going API first.
I'll show you the raw rest pattern, three auth flows,
and three enterprise demos that actually ship.
There's one gotcha that ruins most graph scripts will fix it later.
If you're still loading modules in 2025,
you're heating the office with old exchange servers.
Why PowerShell without modules is the future?
Everything you care about lives in Microsoft Graph now.
Users, groups, devices, Intune Teams, SharePoint licenses, app registrations,
the portal writes graph, your scripts should too.
Rest beats modules because it cuts out the middle mess,
no load times, no dependency, roulette, no version drama.
You call the endpoint, you get the data, you move on with your day.
Token speed credentials full stop.
Oauth2 with search or managed identity gives you short-lived access,
clean audit trails, and automation that doesn't depend on a human
remembering a password they already wrote on a sticky note.
Managed identity means no secrets at all.
That's the point.
Less to steal, less to rotate, less to screw up.
CloudNative means it runs everywhere.
Azure Automation, Functions, GitHub Actions, Containers, Linux, Local,
PowerShell Core is cross-platform, but Graph is the constant.
Curl works on anything with a pulse.
Invogrest method does the job without dragging in the structural integrity
of wet cardboard.
Remember when installing a module meant praying to the new get-gods?
Those days are over.
Benchmarks aren't glamorous, but they're loud.
Load time, modules lag, rest is fire and go.
Cold start in a function or an actions runner.
Rest starts immediately.
Modules sit there thinking.
Reliability, modules choke on throttling or stale tokens you never ask for.
Rest is predictable if you set headers and handle retries.
Portability, Linux and containers don't care about your module drama.
Rest just runs.
Here's the business side because someone will ask about value.
Faster delivery because you don't wait on module updates.
Fewer outages because you control the token and the retry logic.
Easier governance because permissions are explicit and scoped per job,
not hidden inside someone's global profile.
Cost, your cold start, stop wasting minutes, your failures go down.
No works on my laptop nonsense.
The thing most people miss, Graph updates instantly,
modules lag by weeks or never.
New feature? It lands on Graph first.
You can call it today.
Waiting for a module means waiting for a maintainer who isn't on your payroll.
Meanwhile, your project deadline didn't move.
And yes, modules load slower than my ancient exchange server.
Graph doesn't care. It just responds.
You can pin versions on V1.
Test beta endpoints when needed and guarded with feature flags.
You get to control change instead of being surprised by it.
Security teams will actually like this.
These privileged scopes per app registration.
Admin consent reviewed on a schedule.
Search based auth with short lifetimes.
Managed identity where you can.
Search where you must.
Every call leaves a trail.
Request IDs, correlation IDs, who consented to what?
You don't get that from a plain text password stuffed into a script
like a loose wire in a breaker panel.
So why now? Because the cross-platform reality is here.
You're running on Linux, runners, building containers,
pushing jobs into functions.
The module stack was built for a Windows First World
and a simpler set of products.
We don't live there anymore.
All right?
Enough theory.
Here's the pattern you'll use everywhere.
Get a token, set headers, call rest, handle paging,
honor retry after and move on.
It's boring, which is why it works.
The core pattern.
Native PowerShell plus rest plus graph API.
Here's the pattern I promised.
Token, headers, rest, call.
That's the loop.
You'll reuse it for everything from listing users
to smacking non-compliant devices.
Scripts don't fail because of PowerShell.
They fail because of assumptions.
So stop assuming magic.
Build the three pieces every time and you'll sleep at night.
Start with the token.
You've got three ways to get one and they map to real life.
Device code for local testing when it's just you at a console.
Client credentials with a certificate for automation
where no one's clicking anything.
Managed identity when you're in Azure
and you want secrets to disappear like they should have years ago.
Same outcome, different doors.
Device code is the friendly one.
You request a token for HTTPS, graph, Microsoft.com.
With the scopes you need, you get a code, you open a browser,
you confirm it's you and PowerShell gets a token back.
Great for building the first version and poking endpoints.
Bad for production because humans are squishy and forgetful.
Client credentials is the adult path.
You create an app registration.
You granted only the graph application permissions it needs
and you add a certificate.
Your script signs a JWT with that cert
and requests a token using the pass-sass default scope for graph.
No user, no prompts.
Clean audit trail, rotate the cert and move on.
If I see a client secret pasted in plain text again,
I'm revoking Wi-Fi.
Managed identity is the quiet killer.
You enable it on your automation account, function app or VM.
Then you call the local identity endpoint, ask for a graph token
and Azure hands you one tied to that identity.
No vault lookups in your script.
No secrets to rotate.
You just need to grant that identity.
The graph app rolls it requires at least privilege means fewer to a m calls.
Now the headers.
Don't overthink it.
Authorization, bearer your token.
Content type, application, JSON for anything with a body.
When you're doing advanced queries or searches, add consistency.
Level, eventual and the appropriate prefer headers
if the endpoint supports them.
The thing most people miss is they forget the consistency level
and then wonder why their account or filter looks drunk.
Then make the call.
In VogueGress method is fine.
Method, URI,
headers, maybe a body.
The mental model is simple.
Token, headers, call, check, page, retry, continue.
You'll page through results using AdO data.
Next link whenever it shows up.
If you only got 100 items, that's not a mystery.
That's the default page size.
Follow next link until it stops.
Put a guard on your loop so it can't run forever if the API burps.
Now here's where most people mess up.
You must respect throttling.
Graph doesn't care about your feelings, implement retries or enjoy failures.
If you see 420503 look for retry after,
sleep for that duration plus a little jitter
so you don't join a thundering herd, then try again.
Exponential back off beats panic clicking.
If your automation can't survive transient errors, it's not automation.
It's a suggestion.
Common mistakes, so you don't repeat them.
One wrong audience, you ask entra for a token to management.
Azure.com and then called graph, Microsoft.com.
That's a 401, not a conspiracy.
Two pagination denial, why 100 rows only?
Because you never read next link?
Three tight loops without delay.
You angered the throttle gods and now everything slower.
Four over permissioned app with directory.
Read right all just to test.
You just failed and ordered you haven't had yet.
Let me show you the quick wins you can do today.
Device code, grab a token, get me and confirm you can read your own profile.
That proves your token and headers are wired.
Client credentials use default call users,
select id, display name, mail to keep payload small and process a page or two.
Managed identity.
In Azure call intune's device endpoint via graph, set top, follow next link and dump only
id device name and last check in date time.
Good words, bad scripts because contrast helps.
Bad module error, import, fail update, fail, copy, fail.
Good rest error token call done.
Sure, your rapid and functions are at logging, but the backbone is boring on purpose.
One more pro move, build a tiny retry helper and a pagination helper once.
Pass in the your eye and headers, get back the full data set with retries already handled.
Suddenly every script is 20 lines shorter and 10 times calmer.
The game changer nobody talks about is you can test these helpers locally, then drop them
in a container or a function without changing a line.
Done, enterprise demo one, intune device cleanup.
Ten and SWAT devices stack up like abandoned cards in a grocery lot.
Policies get noisy, compliance drifts and suddenly your reports look haunted.
Let's clean it with graph, no modules on a schedule, with logs you can show to security
without blushing.
Here's the plan, we query intune devices from graph where last check and date time is older
than a threshold you set.
We decide action by agent tags, disable if stale, retire if older, delete if fossilized.
And we check ownership first so you don't nuke personal devices because someone missed
a field, boring, predictable, safe.
Start with the end point, you're calling gethttps/graph.
Microsoft.com/beta-divisemanagement-devices-with-select-trim-payload.
ID, device name, operating system, last check and date time, manage device owner type,
as your AD device eat and any tag you rely on.
Use filter for server side cut.
Last check and date time LT24-0101-TZ-UZC-OCE.
If you can't filter exactly how you want, pull with the conservative window and filter
in PowerShell.
You'll get paging.
Follow @odeta.nextlink until it stops.
Guard the loop so it can't spin forever.
Then classification, corporate owned, evaluate action thresholds.
For example, 30 to 60 days, mark for review, 60 to 120, retire, 120 plus delete.
Personal owned, maybe you only notify or tag for review.
The thing most people miss is time skew and inactive but just reprovision devices.
Cross check as your AD device ID against Entra device last seen if you need more confidence.
If there's conflict, skip and lock now actions retire is opposed to manage devices, ID retire.
Delete is delete, manage devices like ID.
Disable often means flipping state where supported or writing a tag and letting policy handle
it, batch where the endpoint supports it but don't stamp it.
Respect for 29503, owner, retry after with jitter.
Write every action to lock analytics, device, ID, action, recent time stamp result, request
ID, correlate with a runead so you can reconstruct the story later.
Horror time.
I watched someone delete 800 devices because they didn't understand last check in timestamps.
They filtered on a property that lagged for re-enrolled devices and skipped dry run.
Graphed it exactly what they asked, it always does, in tune, never lies but boy does it stay
quiet until it's too late, don't be that headline.
Automation setup, use an automation account with a system assigned managed identity, granted
least privilege, graph roles for device read and the specific device actions.
For non-secret config invariables, thresholds, tag names, action map have a feature flag
for dry run.
Dry run writes what it would do not what it did.
Run that first, then run it again, then maybe touch production.
Mistakes to avoid, looking devices during a regional time skew, forgetting to limit by
platform when your Mac fleet reports differently.
Running with directory, read write, all just to test.
No back off policy and hitting global throttle so the next team's job also fails.
Write locks, not feelings, punch line.
If the portal shows it, graph can do it faster, quieter and on schedule.
No module drama, just tokens headers calls.
Enterprise demo 2, identity onboarding via graph only, 450 words.
Onboarding should be boring, if it's exciting something is wrong.
We're wiring HR to identity with graph, so accounts show up, licensed, grouped and ready,
before the manager gets impatient and opens a ticket.
Flow is simple, client credentials with a certificate, not a secret.
Your app registration has only the graph app roles it needs.
User, read write, all if you must create users group.
Read write, all if you must assign membership, directory, read all for lookups and the license
assignment roles.
Admin consented once, reviewed quarterly.
Your script signs the request, asks graph for a token using passgars default for http.graph.microsoft.com
and starts the pipeline.
Step one, create the user.
Post 2, users with minimal attributes.
Account enabled, true, display name, mail nickname, user principle name, usage location
and a temporary password with force change password next sign.
In true, if you're not using SSPR start, keep it lean.
If the user already exists, you patch, not freak out.
It impotency means you can rerun safely after a failure and it won't make a mess.
Step 2, assign a baseline license.
You'll get subscribescuse once, cache the skew map and pick the right skew ID, then post
2, users rush ID, assign license with ad licenses containing the skew ID and disable plans
array if you do selective services, handle quota gracefully.
If you're out of licenses, you log a blocking event and notify the right channel, not
explode the run and leave half created objects.
Step 3, groups by role.
You keep a configuration map from job code or department to static group IDs.
Names drift IDs don't.
You put or post 2, groups slash, group id, members ref with the user's directory object ID.
If the user is already a member, skip.
If the group doesn't exist, that's a configuration failure, not a runtime adventure.
For script keeps moving for other memberships and logs they miss with correlation id.
Step 4, app access.
Many enterprise apps hang off group assignments or app roles.
For app roles, you post to a service principles, a speed, app role assigned to with the user's
object id and the app role id.
For group based SSO, adding the user to the right group is enough.
Again, use IDs from config.
No name, lookups and hotpots, guard rails, correlation id per onboarding.
Every graph call logs request id, URI, method, status, duration, retries.
Retry, back off on 429.5.6, feature flag for dry run, which creates a plan, but does no rights.
Item potency everywhere.
If user exists, patch, if license exists, skip.
If group membership exists, skip.
And for the last time, if I see a client secret hard coded again, I'm revoking Wi-Fi.
User third or manage identity.
Quick win, this runs on a Linux runner with PowerShell Core.
No modular load, no waiting for someone to publish a fix.
User shows up in seconds with baseline access and your help desk doesn't touch a thing.
Now your pipeline is the quiet boring part of onboarding, the way it should be.
Enterprise demo three, compliance drift detection and remediation.
Compliance sprawl is the slow leak that flattens your weak.
Devices drift, users get risky, tickets pile up like snow.
We're going to scan, target, remediate and verify, all with graph, no modules, and without
waking up set-ups at 2 a.m. start with a schedule and a managed identity.
This job isn't special, it just needs to be reliable.
The identity gets only the graph rolls it needs.
Device compliance read, device actions if you remediate identity protection, read for
user risk and the session revoke permission.
Don't grab directory.
Read right.
All just to test.
That's how audits become folklore.
First pass devices call get a tbsgrushishgraph.microsoft.com/v1, device management, device compliance policy,
settings eight, summaries or the device compliance states and point your tenant users.
Use select to keep payload small.
ID, device name, user principle name, operating system compliance state.
Filter where you can, compliance state EQ non-compliant.
You'll get pages, follow adodata.next link, put a guard on the loop, now classify.
Non-compliant doesn't mean execute order 66.
You map severity to action for low severity as maybe a push notification or an email with
a remediation guide, medium trigger a remediation script or force of policy sync.
High, quarantine the device or block access to sensitive apps.
Batch where the endpoint supports it, but keep the degree of parallelism low.
Throttling friendly, not stumpy.
When you act, log like an adult.
For every device you touch, write run ID, device it action reason, request it, status and
latency to log analytics.
If a device flips to compliant during the run, skip and note the flip.
After a remediation, reach agst status.
If it's still non-compliant, escalate once, not five times, alert thresholds, not spam,
users next.
Pull risky users from identity protection via get.
HTTPS, xxgraph, Microsoft.com/v1, identity protection, risky users, filter, risk level
EQ high and selected user principle name risk level risk state for each high risk user
take targeted action, revoke sign in sessions via posts or users to ID, revoke sign in sessions.
If your policy demands it temporarily block sign in, patch users ID with account enabled
false time box and logged mini rant, don't carpet bomb sign in, use severity.
You're not diffusing a movie bomb.
Reality check compliance isn't a state.
It's a drifting target you have to chase.
That's why we use delta where possible.
For device compliance, if delta endpoints exist for your scenario, use them to avoid rescanning
the world.
For users, keep a cache of last process risk change timestamp query only what changed since.
That's how you keep runs under budget and under the throttle radar.
Common mistakes, blanket blocks without a severity filter, congratulations, you just created
a help desk fire drill.
No audit lock.
Now security wants names, times and reasons you can't show.
Hard coded IDs, someone renames a policy and your script face plans.
Identity IDs in config, not code.
If you can't reproduce a run from logs, you're guessing.
Punch line, detect target, remediate, verify, log, quiet, repeatable and boring on purpose.
Architecture breakdown, identity, automation, execution, observability, you've seen the
pattern.
Now why are the plumbing?
So it doesn't fall over when someone sneezes near Azure.
Identity layer first.
Managed identity wherever the workload lives.
Automation account, function app container in ACI, VM, flip it on.
And only the graph app rolls the job needs and stop thinking about secrets.
If you can't use managed identity, fine, and trap registration with a certificate, short
lifetime stored in key vault rotated on a schedule.
No secrets in scripts, not in dev, not just testing, not ever.
Automation layer.
Use Azure automation for simple schedules with runbooks that wake up, do one job and
go back to sleep.
Use functions for event driven flows.
User created device status changed, license inventory dipped.
Small, fast, cold start friendly when you're not dragging modules.
GitHub actions for CICD and cross OS runners stick the same scripts in pipelines that validate
then deploy to prod.
Local power shell for validation and reproducible test before you throw anything at production,
execution layer, power shell core plus invoke rest method.
Version pin your endpoints V1.
For stable, beta only behind feature flags with clear blast radius.
Build two helpers once.
Retry handler that honors 429 503 with exponential back off and jitter and a pager that follows
at or data.
Next link with guards, drop those helpers into every script.
Suddenly your code is small, predictable and not stitched together with three connectors
and a prayer.
Configuration and secrets.
Store non-secret config like group IDs, sqmaps, thresholds, in json or environment variables.
Keep one config per environment so you don't hard code anything that will drift.
Secrets and certs live in key vault.
The code reads via managed identity, not a magic string in a ps1 file.
Feature flags for dry run and confirm impact make rollout safe instead of theatrical.
Observability.
If you can't see your automation, you can't trust your automation.
Send logs to log analytics, request id, correlation id, uri, method, status, duration retry
count.
Trace the whole flow with a run id so you can reconstruct what happened without calling
six people.
App insights for dependencies and live telemetry on functions.
Change your monitor alerts on patterns that matter, failure rate spikes, throttle rate surges,
SLA breaches, not every 404.
Guard rails.
Lease privilege map to jobs, not teams, pin versions.
Review app consent squatterly.
Dry run by default in new environments.
And yes, Microsoft wants you here, not because it's cute, because modules can't keep up.
This stack is faster to ship, simpler to govern and it doesn't panic when you move it from
your laptop to Linux to a container.
That's the point.
Why Microsoft wants you on graph?
Microsoft wants you on graph because it's the one surface they can actually ship to at speed.
Identity devices, apps, content, the portal rights graph, so your code should too.
Features hit graph first.
Modules get love when someone finds the time.
Sure.
So is winning the lottery.
Governance gets cleaner.
O-auth scopes and consent tell you exactly who can do what and every call leaves a trail
you can audit without rummaging through someone's profile script.
Scale.
The endpoints handle global traffic if you handle paging and back off like an adult.
That from reality, Microsoft ships, PowerShell core, but graph is the constant.
The thing most people miss is maintainers don't control product release speed.
Rest schema changes land your code can adopt them that day.
Beta has risk, pin versions and wrap with feature flags.
Permissions will sprawl if you're lazy.
Design roles per job.
The portal is just a pretty face on top of graph.
Don't be the last person to realize it.
A best practices security reliability speed.
Security first.
Use managed identity wherever it exists when it doesn't set off speed secrets.
Microsoft lifetimes rotate on schedule, store in key vault.
Least privileged graph roles per job, not per team.
Quarterly consent reviews or enjoy surprise outages.
Reliability next.
Handle pagination on every list endpoint.
Detect 429503 on a retry after.
Add exponential back off with jitter.
E-dampotency everywhere.
Check before change.
Abset patterns.
Use e-tags when available.
Delta queries cut scan time and cost.
Performance matters.
Use select to trim payloads.
Batch wear supported.
Parallel with limits so you don't stampede the API.
Cashestatic lookups like group IDs and SKU maps with a TTL.
Observability isn't optional.
Log request ID, correlation ID, URI methods, status, duration, retry count.
Keep a run in for multi-step flows.
Track success rate, P95 latency, throttle rate and delta efficiency.
If your automation can't survive a 429, it's not automation, it's a suggestion.
Write logs not feelings code hygiene keeps you sane.
Small functions over 500 line scripts.
Config driven via JSON or environment variables, no hard coded IDs.
Triflex for dry run and save rollout.
And yes, test your back off.
Graph doesn't care about your feelings.
The gotcha that ruins most graph scripts.
Alright, the promised gotcha.
This one ruins more graph scripts than anything else and it's not even interesting.
Wrong token audience.
You ask for a token to management.
As your dot com, then you call graph.
Microsoft dot com.
And you stand there wondering why you got a 401 like it's a plot twist.
It's not.
Fix is dull and absolute.
Audience must match resource.
If you're using client credentials, you request for HTTPS, Graph, Microsoft dot com with
the world default scope device code.
Same story.
Scopes for graph, not something you copied from an Azure arm tutorial in 2018.
Managed identity, ask the local endpoint for graph, not whatever's in the sample.
If you mess up your token audience, don't worry, you'll know immediately.
Graph will reject you faster than a bad Tinder opener.
Bonus trap pagination plus filtering.
Not every property is filterable server side.
If the docs say it isn't, believe them.
Do what you can with filter on supported fields, then finish the cut in PowerShell.
And when you mix search, count or advanced queries, remember the consistency level eventual
header, or you'll get results that feel like they were assembled by a drunk spider.
Sanity checklist before you hit run.
Token audience is graph.
Required scopes granted and admin consented pagination handled with guards.
Retry with back off wired and tested, logging on every call with request id and correlation
id.
Scope it right or enjoy 401s and a long walk through
the path.
You'll see the future of the path.
The future is pure graph.
Here's the take away.
The future of PowerShell is tokens, rest and graph, not modules.
Move one thing to graph this week.
Start with your worst module script.
Ship the token headers call pattern.
Use retries.
Kill secrets.
Then come back for the advanced patterns and my throttle save retry function.
Next episode has the reusable auth wrappers.
Stop living like it's 2016.
Graph is the job now.
Modules are nostalgic.
Graph gets the job done.