AI agents, approval gaps, and the edges of a persona model
Everything in this series so far has assumed the thing being delegated to is a person or a company. That assumption is ageing quickly. An AI agent acting on a specific task, for a specific account, is a genuinely different kind of actor from a carer or an accountant, and treating it as just another login misses the one property that actually matters: an agent's mandate should be impossible to mistake for a standing relationship.
In this post I will detail two extensions that pushed the model from this series somewhere I hadn't originally scoped it for - a fourth principal type for automated actors, and a relation for the gap between a delegation being requested and being approved - plus a vertical that needed no model change at all, which turned out to be the more interesting result of the three.
A stricter kind of principal
person and business are never security principals in this model - only a persona or a business#staff roster ever holds a role. agent goes one step further: it's not just excluded from being an unconditioned principal, the schema won't let it hold a standing role at all, full stop.
$ fga model test --tests <a file granting an agent the viewer relation>
Error: Invalid tuple '...#viewer@agent:bot'. Reason: type
'agent' is not an allowed type restriction for 'account#viewer'
An agent is only ever a valid grantee on delegate, and the condition it's checked against combines two things rather than one:
condition task_scoped(current_time: timestamp, valid_until: timestamp, task_id: string, authorized_task_id: string) {
current_time < valid_until && task_id == authorized_task_id
}

Oscar's accounting firm offers an AI reconciliation agent as part of its service. Oscar authorises the agent - not the firm, not a named accountant - for the task q3-2026-reconciliation, until a specific date. Invoke the same agent for q4-2026-reconciliation inside the same time window and it's denied exactly as if the window had already closed; get the timing wrong instead and it doesn't matter that the task was named correctly. Both have to hold. That's the strongest version of "an agent's mandate is temporary and specific, never a standing relationship" this model can express - not a convention someone could accidentally violate, a constraint the schema itself enforces.
The gap between asking and being allowed
Every delegation earlier in this series existed the moment its tuple did. Real approval flows - the kind covered by Auth0's CIBA support in an earlier series - have a state in between: requested, but not yet decided. pending_delegate represents exactly that gap, and it uses the same trick as individual and business earlier in this series - it's a fact the application can list and act on, and it grants nothing at all.
- user: persona:farah-healthlink
relation: pending_delegate
object: account:jane-patient-record
Farah's request sits on the account, visible to the application, with precisely the same access as a stranger until Jane decides. approver identifies who can act on it - Jane, as owner, always can; Farah, as the requester, can't approve her own request. On approval, the application deletes the pending tuple and writes a real, conditioned delegate tuple in its place; on denial, it just deletes it. The request itself - who asked, when, why - stays in the application's own data store the entire time, never in FGA, which is the same architecture the CIBA post used for approval-request state generally.
The vertical that needed nothing new
Acme Supplies is a direct customer of one platform and, completely separately, an engaged supplier-integration partner on a different platform entirely, run by a different company. Neither relationship needed a model change: business was never tied to a specific service in the first place, so business:acme-supplies#staff can be written as the grantee on an account under any service, exactly the same primitive as the accounting and law firm engagements from earlier posts.

What the exercise did surface, unplanned, was a genuinely counterintuitive result. Raj, an ordinary staff member at Acme, has less authority over Acme's own storefront - viewer only, because the structural business-to-account link only promotes a business's admins to account-admin, not its ordinary staff - than he has over a completely different company's account on a different platform, where the engagement names business#staff directly and gives him full admin. Which of the two mechanisms applies decides the tier. "He works for Acme" doesn't imply a consistent access level across every account Acme touches, and a model that pretended otherwise would have been wrong in a way that's easy to miss until you actually check both accounts side by side.
Next Steps
Five posts in, the model has grown a handful of relations and one new type, and nothing earlier in the series had to change to accommodate any of it. A final post in this series is a different kind of post entirely - how this model was actually built, where an adversarial review across two different AI models caught a real OpenFGA semantics bug and also produced confident, specific-sounding nonsense that didn't survive being checked.