IAM agents are easy to prototype and easy to over-trust.
My first versions could answer useful questions, but too much retrieval, computation, and reporting was happening inside the LLM.
At first, I spent a lot of time thinking about the agent itself. Which model should I use? Which harness? What tools should I give it? How should I structure the prompts? Should I use sub-agents?
Those decisions matter, but they weren't the part that made the system work.
The most important part of my IAM agent isn't the LLM or the harness. It's the IAM data and analytics layer underneath them.
I didn't really build an LLM with a bunch of IAM tools attached to it. I built an IAM data and analytics layer first, then gave an LLM a controlled way to interact with it.
That changed how I thought about the whole architecture.
Once I separated the responsibilities, the architecture got simpler. The LLM interprets the question, skills point it at a known IAM workflow, and specialists narrow the problem. The facts still come from deterministic code and the data underneath it. Grounding gives those facts the context they need to mean something inside my environment.
The model sits on top of all of that.
It isn't the system.
What should the LLM actually be doing?
Earlier versions of the agent did too much inside the model.
The LLM could retrieve information, reason across it, calculate things, and then turn everything into a nice report.
And, to be fair, it worked surprisingly well.
Until it didn't.
IAM is not a great place for "usually correct."
If the model fails to retrieve a source and concludes that a user doesn't have access, that's not just a bad chatbot answer. That's a bad security conclusion.
I ran into similar problems with data interpretation. A timestamp might represent when my pipeline collected a record, but the model could easily interpret it as when the access was assigned.
SQL was another one. The model is perfectly capable of writing a query, but if I already know how a particular IAM question should be answered, why let the model invent a slightly different query every time?
I started pulling those responsibilities away from the model.
The rule I landed on was:
The LLM can orchestrate and interpret. It should not be the source of IAM facts.
If I can retrieve or calculate something deterministically, I'd rather do that outside the model.
Start with the questions
Before building more agent capabilities, I needed to understand what I actually wanted the system to answer.
Things like:
- What access does this person have, and why?
- Who has access to this application?
- Who has access to the company's code base?
- Who has admin or privileged access?
- What does our MFA enrollment posture look like?
- Does this group's membership make sense for what the group is supposed to do?
- If someone's department, role, or team changes, what happens to their access?
- Are our authentication and access controls behaving the way we expect?
Those questions start to tell you what data matters.
For me, that means things like user attributes, groups, group automation, access policies, application assignments, roles and permissions, cloud access, code access, privileged access, MFA enrollment, and authentication or authorization events.
Your sources will be different. That's fine. What matters is understanding how they relate.
For example:
User Attributes
│
▼
Access Rule / Policy
│
▼
Group
│
▼
Application Assignment
│
▼
Role / Permission
Once I can represent those relationships, I have something far more useful than a collection of API responses.
I have data I can reason over.
The IAM data layer
Every system has its own view of identity.
An IdP such as Okta might give me users, attributes, groups, group rules, MFA enrollment, and application assignments.
GCP or AWS gives me another view of access.
GitHub tells me who can access the company's code base and where that access exists. GitLab can provide a similar view in another environment.
A privileged-access system can tell me what elevated access someone can obtain or is actually using.
Logs tell me what happened.
The interesting IAM questions tend to cross those boundaries.
What does this person actually have access to, and why?
No single system necessarily owns that answer.
So I built an analytical IAM data layer that gives me a consistent way to work with the identity relationships I care about.
In my implementation, I use BigQuery. I push a lot of the population-scale querying into BigQuery's compute engine and use Python and Pandas where they make sense.
That's just my implementation.
You could use Snowflake, Databricks, Postgres, or something else entirely.
The technology isn't the point.
The important part is having a queryable and reproducible representation of your identity environment that can answer the questions you care about.
The same data can tell much richer stories than simple access lookup. MFA enrollment posture, privileged-access usage, expected group populations, access-policy gaps, or a relationship between a group automation and a SCIM-provisioned application are all examples.
I don't need the agent to deeply reason through every one of those relationships on its own. I need the data layer to make them available when the question calls for them.
Compute broadly, reason narrowly
This was probably one of the biggest lessons for me.
At first, I was putting too much IAM data into the model.
IAM data gets large quickly. Users, groups, assignments, permissions, policies, and logs can eat through a context window faster than you might expect.
I could give the model a large dataset and ask it to find something interesting.
But eventually I had to ask myself why I was doing that.
If I want to test a control across 40,000 users, an analytical engine is much better at searching 40,000 users than an LLM is.
I don't need to give the model 40,000 records.
I need to give it the 27 that matter.
That made the system better in a few ways.
It used fewer tokens.
It kept a lot of noise out of the context.
The model had a smaller problem to think about.
And, importantly, the calculation was now happening somewhere I could test it.
I started thinking about the analytical layer as part of context engineering.
I'm not just calculating an answer before handing it to the model. I'm deciding what evidence the model actually needs.
The shorthand I use for this now is:
Compute broadly. Reason narrowly.
If I know how to calculate it, I put it in code
Once I find myself answering the same IAM question repeatedly, I don't want the model rebuilding the logic each time.
I turn it into a capability.
For me, that's usually parameterized SQL and Python.
Something along the lines of:
get_effective_access(user_id)
get_code_access(user_id)
get_privileged_access(user_id)
get_mfa_posture(population)
find_users_outside_expected_population(group_id)
These aren't necessarily the literal interfaces in my codebase, but they show the pattern.
Now I have a known input, a known source, and logic I can test and reproduce.
If two people ask the same question against the same snapshot, I don't want the answer to depend on which SQL statement the model happened to come up with that day.
The model should work out what it needs to know.
Code should work out the answer where that's possible.
Skills are what connect the two
Of course, nobody using the agent is going to ask:
Run
get_effective_access()for Alice.
They're going to ask:
What access does Alice have?
This is where skills become important.
I've already worked out how I want that investigation to happen. The skill gives the agent that path.
Conceptually:
"What access does Alice have?"
│
▼
User Access Skill
│
┌───────┼────────┐
▼ ▼ ▼
Apps Code Privilege
Groups Cloud Admin
│ │ │
└───────┼────────┘
▼
Deterministic Scripts
│
▼
Structured Result
The skill is what connects the natural-language request to the right scripts and IAM data.
That's an important part of the design.
If I already know how a user-access investigation should work, I don't need the LLM inventing a new investigation plan every time somebody asks the question.
I've encoded the path once.
Conceptually:
intent → skill → deterministic capability
The implementation changes depending on the harness, but the job of the skill stays the same.
Specialists keep the problem smaller
I also use specialists for particular IAM domains.
I have different areas of the system that understand things like user access, cloud IAM, admin access, privileged access, and authentication data.
What that looks like depends on the harness.
In one environment it might be a sub-agent. In another it could be a custom prompt, delegated task, or some other framework-specific primitive.
I don't think that distinction matters much.
What matters is keeping the responsibility narrow.
A specialist gathers and structures the information for its domain and can apply the relevant organizational context.
What I don't want is every specialist independently inventing its own IAM logic.
Gather first. Validate it. Then present it.
Another change that made the system much more reliable was separating data gathering from reporting.
Earlier versions could collect information and immediately turn it into a polished response.
Again, that looked great.
But now retrieval, computation, interpretation, and presentation were all mixed together.
I changed the flow to:
Gather
│
▼
Structured Result
│
▼
Validate
│
▼
Format
│
▼
Interpret
Specialists return structured data.
I validate that the result has the shape I expect.
Deterministic formatters handle factual output.
Then the model can help interpret the result.
It's less magical.
That's a good thing.
For IAM, boring and correct is a feature.
One small part of this pattern ended up being particularly valuable.
I explicitly distinguish between:
[] I checked and found nothing.
None I couldn't determine the answer.
missing Something in the implementation is broken.
Those are three very different states.
If my privileged-access query fails, the agent cannot turn that into:
Alice has no privileged access.
It has to tell me that it couldn't determine the answer.
That sounds like a small implementation detail, but this is exactly the kind of thing that can turn a failed lookup into a bad security conclusion.
The data still needs context
Even if every record in the data layer is correct, there's another problem.
The data doesn't always explain itself.
Suppose the IdP tells me:
Alice → engineering-platform-users
Great.
But what is that group for?
Who is supposed to be in it?
What application does it grant?
What permissions come with it?
Is the application high risk?
Does joining the group trigger provisioning into another system?
A lot of that meaning is specific to the company.
So I maintain grounding data for things like application purpose, categories, expected populations, provisioning methods, permission meaning, risk, access policies, and known exceptions.
I keep that information in version-controlled configuration so it can be reviewed and maintained.
One simple example is knowing that a group is linked to an application that uses SCIM.
If someone is investigating an automation change for that group, the agent can surface the relationship. A bad change might not just add somebody to the wrong group. It could trigger a downstream account or permission change.
I don't need the LLM to infer that relationship.
I've already given the system the context.
That's the difference between retrieving IAM data and understanding what that data represents.
This is where SMEs matter
A model doesn't know why my organization created a particular group or what an application owner considers appropriate access.
Quite often, the source system doesn't know either.
That information lives with people. IAM engineers, application owners, security analysts, and other subject-matter experts.
I want their knowledge represented in the system rather than buried in someone's head or copied into a giant prompt.
Once that context becomes versioned data, it can have an owner. It can be reviewed. It can change. And more than one skill can use it.
A schema tells me what data I have.
Grounding tells me what that data means here.
Semantics matter too
Some of the most useful grounding is almost boringly simple.
Timestamps are a good example.
Say I have:
user: alice@example.com
role: admin
snapshot_at: 2026-08-10T03:00:00Z
2026-08-10 is when my collection job observed the role.
It is not necessarily when Alice became an admin.
If I don't make that explicit, the model has a perfectly reasonable interpretation available to it:
Alice was granted admin access on August 10.
Except that's wrong.
So my data documentation doesn't just say:
snapshot_at: timestamp
It says what that timestamp means and, just as importantly, what it does not mean.
That has been far more valuable than I expected.
Sometimes I still need the source system
Having an analytical layer doesn't mean every question should come from it.
Sometimes freshness matters more than scale.
That's why I keep both an analytical path and a real-time path through APIs, MCP tools, or other integrations.
If I'm measuring MFA enrollment across a large population, I probably want the analytical path.
If I'm investigating an incident and need to know whether an assignment exists right now, I may go back to the source.
If I'm looking at privileged-access usage over the last 90 days, I'm back in the analytical layer.
The question decides the path.
This is also why I track values such as snapshot_at and ingested_at.
The agent should know how fresh the evidence is instead of quietly treating every record as current.
The harness can change
I don't consider Pi, Claude Code, Google ADK, or another agent framework to be the core of this architecture.
They're ways into it.
One harness might have first-class skills.
Another might use custom prompts.
Another might expose sub-agents, tools, or extensions.
That affects how I wire the interface together, but it shouldn't change the IAM logic underneath it.
The compute stays in portable deterministic operations with structured inputs and outputs.
The harness decides how the model gets there.
That's useful because these frameworks are going to keep changing.
I don't want to rebuild my IAM system every time they do.
One important boundary
The agent I've described here is primarily a human-operated CLI tool.
That's important.
The person running it is still the security principal, and the agent uses that person's existing access.
If I can't query a sensitive IAM dataset, the agent shouldn't suddenly be able to query it for me.
IAM data can contain employee information, access relationships, admin access, and privileged-access data. I didn't want the agent to become a new way around the authorization controls that already exist.
A shared or autonomous IAM agent is a different problem.
So is an agent that can start changing access rather than only investigating it.
Those introduce different questions around agent identity, caller permissions, approvals, execution controls, and auditing.
They're worth going into, but they're separate from the problem I was solving here.
Hallucination became a systems problem
At some point I stopped thinking about hallucination primarily as a prompt problem.
I could keep making the system prompt longer, or I could remove opportunities for the model to invent important facts in the first place.
I chose the latter.
| Failure mode | What I do instead |
|---|---|
| Model analyzes a large IAM population | Use deterministic analytics |
| Model invents access logic | Use tested parameterized functions |
| Model writes a different query each time | Use known query interfaces |
| Failed query becomes "no access" | Preserve explicit error states |
| Numbers change while becoming prose | Use deterministic formatting |
| Model misunderstands a field | Document the semantics |
| Model guesses group or app purpose | Ground it in organizational context |
| Model misses access-policy context | Put that context in grounding data |
| Old data looks current | Track snapshot and ingestion time |
| Specialist returns the wrong structure | Validate before rendering |
None of this means the model can never be wrong.
Of course it can.
The point is to reduce how many opportunities it has to be wrong about something that should have been deterministic in the first place.
If I were starting again
My order of operations would be pretty simple.
Start with the IAM questions people actually need answered.
Work out which identity relationships are required to answer them.
Build an analytical layer that can represent those relationships.
Explore and test the analysis without an LLM first.
Turn the useful, repeatable parts into deterministic capabilities.
Add the organization-specific context those capabilities need.
Create skills that map natural-language intent to the right workflows.
Use specialists where they make the problem easier to reason about.
Keep a real-time path for questions that need it.
Make failure and freshness explicit.
Then put an LLM on top.
At that point, the model isn't being asked to magically understand the identity environment.
You've already done the hard part.
The LLM is the interface, not the source of truth
I started this project thinking I was building an IAM agent.
What I really ended up building was an IAM system that an LLM could reason over.
That's a subtle difference, but I think it's the important one.
When the agent tells me that 37 identities don't match a control, I don't want 37 to be a number the model came up with.
I want the analytical layer to have found 37 identities.
When it tells me a group's population looks wrong, I want that conclusion tied back to what the group is actually for and who should be in it.
When it surfaces that an access rule feeds a SCIM-provisioned application, I want that relationship to exist in the underlying data and grounding.
And when the system doesn't know something, I want it to say that instead of filling in the blank.
The LLM gives me a natural way to ask questions of all of this.
Pi, Claude Code, Google ADK, or whatever comes next can give me the interface.
Skills connect my question to the right workflow.
Specialists help narrow the investigation.
But the part I care most about stays underneath all of them:
the IAM data, the deterministic analytics, the grounding, and the reusable capabilities.
If I were giving someone one piece of advice before they started building their own IAM agent, it would be this:
Don't start by giving an LLM a bunch of IAM tools. Build the IAM data and analytics layer you wish you already had, then let the LLM reason over that.
How I used AI
I used AI as a sounding board and editor while developing this article. The architecture, implementation choices, examples, and lessons come from building the IAM agent itself. I used AI to challenge the framing, tighten the writing, and help turn my notes and diagrams into a clearer explanation of the design.