Freedom Is Conserved: What LLMs and Identity Platforms Have in Common

I had two conversations in the same week that turned out to be the same conversation.

I had two conversations in the same week that turned out to be the same conversation.

The first was with an enterprise customer who wanted to run a small piece of custom code during login. Nothing dramatic — enrich a token with a value from their HR system. The request was reasonable, the business need was real, and the answer was no.

The second was with an engineer frustrated that a model kept returning inconsistent output. They'd written a long, thoughtful prompt asking for an analysis, and were getting prose when they wanted data, different shapes on different runs, and occasional confident nonsense. Their instinct was that the model needed more freedom — a longer prompt, more context, fewer restrictions on how to respond.

The fix was the opposite. Define a schema. Constrain the output. Give it tools with typed parameters instead of an open field. The moment they did, the same model started producing reliable, parseable, boringly correct results.

Those two problems have the same shape, and noticing it changed how I think about interface design in both domains.

The shared failure: unbounded input space

Here's what a free-form prompt and a plugin API have in common: neither one lets the receiving system know what it's being asked to do until it's already doing it.

A model handed unstructured text has to infer intent, format, scope, and stopping condition simultaneously. It might get all four right. Nothing in the interface requires it to. Every run is a fresh negotiation with an unbounded input space, which is why the outputs vary and why debugging feels like arguing with weather.

An identity platform handed arbitrary customer code is in the identical position. It cannot know whether that code will make a network call, block for three seconds, allocate a gigabyte, throw an exception type it's never seen, or silently return stale data. It finds out at runtime, in production, on the login path.

In both cases, the system's behavior is now a function of something it can't inspect, validate, test, or reason about in advance. Every property you'd like to guarantee — determinism, latency bounds, correctness of shape — becomes conditional on input nobody constrained.

The failure mode is the same and so is the fix: replace an open channel with an enumerated one.

The conservation law

Here's the part I think is genuinely underappreciated, in both domains.

Flexibility isn't created or destroyed at an interface. It moves.

Every degree of freedom you give the caller is a degree of freedom you take from yourself as the implementer. And every constraint you place on the caller is freedom you get back.

When a customer can upload arbitrary code into your identity runtime, they've gained enormous flexibility. You have simultaneously lost the ability to refactor that runtime, change internal data structures, upgrade dependencies that leak through the interface, or make any performance claim about the system. Their freedom came out of your budget.

When you constrain them to declarative claim mapping and a policy language, they've lost the ability to do arbitrary things. You've gained the ability to rewrite the entire evaluation engine, run it on different infrastructure, cache aggressively, analyze policies statically, and give a real answer when someone asks how fast login is.

The same trade runs through LLM interface design. A free-text prompt gives the caller maximum expressiveness and gives you, the system builder, almost nothing — you can't validate the request, can't route it intelligently, can't swap the model without re-testing every phrasing someone came to depend on. Define a tool with typed parameters and you've constrained the caller, yes, and in exchange you can validate inputs, version the interface, change the model underneath, and know what the failure modes are.

flowchart LR
    subgraph Open["Open interface"]
        C1["Caller: maximum freedom"] --> B1["Builder: cannot change,\ncannot verify, cannot promise"]
    end
    subgraph Closed["Enumerated interface"]
        C2["Caller: bounded freedom"] --> B2["Builder: can refactor,\nvalidate, optimize, guarantee"]
    end

Neither position is automatically correct. But the trade is real, it's zero-sum at the interface, and the mistake is making it accidentally — handing away your own future flexibility one reasonable request at a time, without ever noticing you were spending something.

Escape hatches become the main road

This is the dynamic that catches people out in both domains, and it's worth naming because it's counterintuitive.

You build a well-designed, constrained interface. Then, sensibly, you add one escape hatch for the cases the constrained interface doesn't cover. A generic plugin point. A free-text field on an otherwise typed API. A run_arbitrary_query tool sitting next to your six specific ones.

The escape hatch does not stay an escape hatch. It becomes the interface.

It happens because the escape hatch is always the path of least resistance. It requires no one to think about whether their case fits the model. It never returns "unsupported." For a customer, writing a plugin is easier than working out how to express their need in your policy language. For a model, a general-purpose tool that plausibly covers the request is a lower-effort choice than reasoning about which of six specific tools applies — so it reaches for the general one, and now every call flows through the least constrained path you built.

In both cases you end up with an interface whose documented shape is structured and whose actual shape is whatever the escape hatch permits. Every guarantee you attached to the structured part now applies to a minority of real traffic.

The practical lesson is the same in both: if you provide a general-purpose escape hatch alongside specific capabilities, expect it to dominate. Either don't provide it, or make it deliberately less convenient than the structured path, or accept that it is your interface and design accordingly. What doesn't work is providing it and assuming people will use it only in the rare cases you had in mind.

Constraints are what let you change the implementation

The deepest version of this argument isn't about correctness or safety. It's about time.

Behind a tool schema, you can swap models. Change providers. Fine-tune. Add caching. Route different requests to different backends. The caller doesn't know and doesn't need to, because their contract was the schema, not the implementation.

Behind free-form prompting, you can't do any of that safely, because callers have come to depend on incidental behaviors of the specific model — its verbosity, its formatting habits, how it handles an ambiguous instruction. Hyrum's Law doesn't care that those behaviors were never promised. Change the model and things break in ways nobody can enumerate in advance.

Identity has exactly the same structure. Behind declarative configuration, you can rewrite the runtime, change the storage layer, introduce caching, move to a different deployment topology. Behind a plugin API, you're pinned — because the moment a customer's code compiled against your interface, your internals became a public contract, including the parts you never documented.

So the constraint isn't primarily protecting the system from bad input. It's protecting your ability to keep building. That's the argument that matters over a five-year horizon, and it's the one that's hardest to make in the room, because the cost is entirely in the future and the benefit of saying yes is immediate.

Where the analogy breaks

It would be too neat to leave it there, and the differences are instructive.

LLMs are nondeterministic by nature; identity platforms are nondeterministic only by choice. When you constrain a model's output, you're recovering predictability the system never had. When you constrain an identity platform's extension surface, you're preserving predictability you started with. Same technique, different direction — one is remediation, the other is protection. That's why the identity case is, if anything, more clear-cut: you're not buying reliability, you're declining to sell it.

Over-constraining costs you differently. A too-rigid output schema genuinely prevents a model from giving you a good answer — force everything into three enum values and you'll lose the nuance that made the model worth using. Over-constraining an identity platform mostly costs you deals and customer goodwill, not correctness. Both are real costs; only one of them degrades the quality of the result.

And the escape hatch is sometimes right for LLMs in a way it rarely is for identity. Open-ended reasoning is often the actual product. In identity, open-ended execution on the login path essentially never is.

So the analogy is a lens, not a proof. It's useful because it makes a familiar trade visible in an unfamiliar setting — most engineers have now felt the "structure the output and it suddenly works" moment firsthand, and that intuition transfers.

Good constraints enumerate; they don't subtract

The objection to all of this is that constraints reduce what's possible. Sometimes true, usually not — and the distinction is where interface design actually happens.

Tool calling didn't make models less capable. It made them reliably capable, by replacing "do whatever you infer from this text" with "here are the things you can do, precisely specified." The capability surface didn't shrink. It got named.

The same move works in identity. Customers asking for plugins are rarely asking for arbitrary computation as an end in itself. They want a specific outcome: transform this claim, derive that attribute, apply this conditional rule, notify that system. Those are enumerable. A constrained expression language, a policy engine, async events, and pre-synchronized attributes cover the overwhelming majority — not by refusing the underlying need, but by giving each need a defined shape instead of a blank one.

That's the difference between a constrained interface and a limited one. Limited means "you can't do that." Constrained means "you can do that, this way." The first loses customers. The second is just architecture.

Which brings the two conversations back together. The customer who wanted to run code and the engineer who wanted a longer prompt were both asking for the same thing — an open channel, because open channels feel powerful. And in both cases the better answer was the same: not a smaller capability, but a named one.

Structure isn't the enemy of capability. In systems where you can't inspect what you're being asked to do, it's the only thing that makes capability dependable.


Disclosure: I work on an identity platform that deliberately doesn't allow customer code execution in the runtime, so I'm not a neutral party on the second half of this. The LLM half I arrived at independently, and it's what convinced me the identity argument generalizes.