Persona Management
This guide explains FlowPilot's persona model, including the data model, API operations, authorization rules, and technical implementation details.
Overview
A persona represents a business role that a user assumes in a specific context. Personas are the core abstraction in FlowPilot’s authorization model and directly drive policy decisions for both autonomous AI execution and delegated access.
Rather than binding static permissions to user accounts, FlowPilot evaluates authorization against the selected persona provided with each request. Persona selection is not a platform-wide setting; it is done and maintained by the client app. The client app determines which persona is selected; either automatically, e.g. based on the app's intended audience, or explicitly via a user-driven persona selection.
Key concepts
- A single user may have multiple personas (for example,
travelerandtravel-agent) - Each authorization request is evaluated against exactly one selected persona
- Personas have explicit lifecycle state with temporal validity (
valid_from,valid_till) and operational status (configurable and policy-relevant, e.g.active,inactive,suspended,pending,revoked) - Personas also encapsulate user's preferences, such as consent for autonomous decisions, limits on cost or effort and risk tolerance levels, email address to be used for profession al purposes, etc.
- Personas are user-owned, independently configurable and their assignment is possibly subject to a strict onboarding process
Why Personas Matter
Introducing personas allows authorization policies to depend on business intent, not on technical account and application structure.
A FlowPilot persona models the business role, and in fact the mandate a person holds at a given moment. Each persona carries only the attributes relevant to that mandate and can be self-declared and managed independently from the underlying user account.
This enables several important architectural properties.
1. Persona-level lifecycle management
User lifecycle management moves from the account level to the persona level:
- When someone leaves a role or function, only the corresponding persona needs to be deactivated
- The underlying user account can remain intact
- Users can onboard early and activate personas only when appropriate
This avoids account churn and reduces operational friction.
2. Delegated security administration
Persona ownership enables delegated security administration:
- Users manage their own personas and persona attributes
- Security decisions are made closest to the source of truth
- No central administrator is required to continuously assign roles, groups, or permissions
This improves:
- timeliness
- accuracy
- appropriateness
- freshness of authorization data
3. Segregation of duties by design
Segregation of duties becomes straightforward:
- Different duties are represented as different personas
- Policies can enforce that certain actions require distinct personas
- Conflicts are prevented structurally rather than procedurally
4. User Managed Access
Personas provide a clean foundation for users managing access in a policy-constrained way:
- Users explicitly control which personas are active
- Delegation operates on personas, not raw identities
- Users are still subject to policies that remain declarative and auditable
Architectural Impact
FlowPilot personas combine the strengths of RBAC, ABAC and ReBAC:
- RBAC-like clarity through named business roles
- ABAC-like flexibility through persona-specific attributes
- ReBAC-like delegation through persona-driven relationships
At the same time, persona-based administration significantly simplifies access management at scale:
- No global role explosion
- No central permission assignment bottleneck
- No need to expose identity or PII to policies
Instead, authorization becomes:
- explicit
- decentralized
- policy-driven
- scalable by design
In FlowPilot, personas place users at the center of their own authorization model, controlling not just who they are, but how and when they may act.
Persona Data Model
Core Attributes
| Field | Type | Description |
|---|---|---|
persona_id |
String | Composite unique identifier: {user_sub}_{title}_{circle} - enforces uniqueness at database level |
user_sub |
String | Owner's user ID (Firebase UID or Keycloak sub) |
title |
String | Persona title (e.g., "traveler", "travel-agent") |
circle |
String | Circle/community/business unit for which this persona is valid (e.g., "family", "acme-corp", "marketing-team") |
status |
String | Current status: defined in policy manifest (e.g., "pending", "active", "inactive", "suspended", "revoked") |
consent |
Boolean | Whether user allows autonomous execution of workflows for this persona |
valid_from |
ISO 8601 | Timestamp when persona becomes active |
valid_till |
ISO 8601 | Timestamp when persona expires |
created_at |
ISO 8601 | Creation timestamp |
updated_at |
ISO 8601 | Last modification timestamp |
Important: Each user can have multiple personas with the same title, as long as they have different circle values. The persona_id is a composite key formed from user_sub, title, and circle. This enforces uniqueness at the database level based on the combination of (user, title, circle).
The Circle Attribute
The circle attribute represents the family, business unit, club, community, association, followship, or circle of trust for which a persona is valid. This allows users to have different instances of the same persona titleand thus role/mandate for different contexts.
Examples:
- A user might have a "traveler" persona for "family-williamson" and another "traveler" persona for their "corsica" travel mates
- A travel agent might indiciate their agency: "travel-agent" with
circle"best-travels" versus "cheap-travels" - An office manager might be working for "marketing-team", while another person may indicate "sales-team" for their
circle
The combination of persona title and circle enables:
- Context-specific preferences: Different autobook limits for personal vs. business travel
- Segregation of duties: Clear separation between different organizational contexts
- Multi-tenancy at the persona level: Users can participate in multiple organizations and clubs with the same role
- Fine-grained access control: Policies can enforce rules based on circle membership
In addition to the persona lifecycle management attributes, each persona can also have a number of custom attributes specific to the business use case. These are configured in the manifest (see below).
Persona Configuration
The persona titles, statuses and custom attributes are defined in the policy manifest as the single source of truth for both authorization policies and profil/persona management.
A system-defined persona title is ai-agent. This is used by the back-end and by agentic AI systems.
Configuration Location: infra/opa/policies/travel/manifest.yaml
Key Benefits of using the manifest:
- Customizable - Can easily be customized for a specific business use case
- Single source of truth - All persona configuration in one manifest file and used by both applications and the policy
- Centralized updates - Change persona configuration in one place
- Policy alignment - Persona titles and statuses automatically match OPA policy expectations, which is use case specific
Important Note on validation and defaulting responsibility
- persona-api: Validates that required attributes are PRESENT (not None) and sets a default value when not present. Does NOT validate ranges or policy-specific constraints
- authz-api: Assumes persona-api did its job and is not bypassed. So it passes attributes to OPA without validation or defaulting
- OPA policy: Performs ALL policy-specific validation (e.g., range checks) that is specific to the business use case
This separation ensures the system is extensible to other policies and business use cases without hardcoding policy logic in the API layer. This makes the platform truly a generic SaaS service
Manifest Structure
The manifest's persona_config section defines status, title and a series of custom attributes. For the "travel" use case, FlowPilot defined the following status, title and autobook attributes. The latter enable and steer autonomous AI booking. Their values are validated and normalized by the persona-api and coerced to the type specified. Their values are also defaulted in case they are optional and a default value is provided. A default value of null means that the attribute will only be created when it explicitly has a value.
persona_config:
# Allowed persona status values (lifecycle states)
persona_statuses:
- pending # Persona created but not yet activated
- active # Persona is active and can be used
- inactive # Persona is disabled by user
- suspended # Persona temporarily disabled by admin (e.g., suspected behaviour)
- revoked # Persona permanently disabled (e.g., mistake or compliance issue)
# Persona titles with rich metadata
persona_titles:
- title: visitor
description: "End user who may be interested in travel options or the itinerary"
can-be-invited: true
can-be-delegated-to: false
allowed-actions: [read]
- title: traveler
description: "End users who book travel for themselves and for whom autobook preferences apply to their itineraries"
can-be-invited: true
can-be-delegated-to: false
allowed-actions: [read, create, update, execute, delete]
- title: business-traveler
description: "End users who book travel for business purposes with specific autobook preferences"
can-be-invited: true
can-be-delegated-to: false
allowed-actions: [read, create, update, execute, delete]
- title: travel-agent
description: "Travel agent who can execute workflows on behalf of travelers, if explicitly delegated"
can-be-invited: false
can-be-delegated-to: true
allowed-actions: [read, create, update, execute, delete]
- title: office-manager
description: "Office manager who can consult and update someone's booking, but cannot execute it"
can-be-invited: false
can-be-delegated-to: true
allowed-actions: [read, create, update]
- title: booking-assistant
description: "Booking assistant who can manage and execute bookings on behalf of travelers"
can-be-invited: false
can-be-delegated-to: true
allowed-actions: [read, create, update, execute]
- title: user-admin
description: "Supra-level administrator with full permissions to update personas of other users"
can-be-invited: false
can-be-delegated-to: true
allowed-actions: [read, create, update, execute, delete]
# Persona custom attributes
attributes:
- name: autobook_price
type: integer
source: persona
default: 500
required: false # when not given, the default value is set
description: "Maximum trip cost for autonomous booking (EUR)"
- name: autobook_leadtime
type: integer
source: persona
default: 7
required: false
description: "Minimum days before departure for autonomous booking"
- name: autobook_risklevel
type: integer
source: persona
default: 3
required: false
description: "Maximum airline risk score for autonomous booking (1-5 scale)"
- name: business_email
type: email
source: persona
default: null
required: false
description: "Business email address for the persona"
Architecture Flow
manifest.yaml (SOURCE OF TRUTH)
↓
├─→ Python Services (via persona_config.py)
│ ├─→ persona-api (validates titles & statuses)
│ └─→ authz-api (loads attribute schema)
│
└─→ [generation script] → persona_config.json
↓
OPA Policy Engine
The persona_config.json file is auto-generated from manifest.yaml:
- For local development: Run make generate-opa-config
- For Docker/Cloud Run: Auto-generated during build process
- Never edit persona_config.json directly - it will be overwritten
Example Persona Document
{
"persona_id": "PKbHpCqDnLcNywEo8pev8yQmoU43_business-traveler_acme-corp",
"user_sub": "PKbHpCqDnLcNywEo8pev8yQmoU43",
"title": "business-traveler",
"circle": "acme-corp",
"status": "active",
"valid_from": "2026-01-11T17:00:00Z",
"valid_till": "2026-12-31T23:59:59Z",
"created_at": "2026-01-11T17:00:00Z",
"updated_at": "2026-01-13T17:00:00Z",
"consent": true,
"autobook_price": 10000,
"autobook_leadtime": 7,
"autobook_risklevel": 5
}
Note the persona_id format: {user_sub}_{title}_{circle}. This composite ID ensures that each user can have multiple personas with the same title, differentiated by circle.
Persona Lifecycle
Persona-API
Purpose: Persona lifecycle management (CRUD operations)
Endpoints:
POST /v1/personas- Create persona (idempotent)GET /v1/personas- List user's personasGET /v1/personas/{persona_id}- Get specific personaPUT /v1/personas/{persona_id}- Update personaDELETE /v1/personas/{persona_id}- Delete personaGET /v1/users/{user_sub}/personas- List personas for any user (service accounts only)
Authorization:
- User endpoints: JWT
submust match persona owner - Service endpoint: Requires service account token (Keycloak:
client_id=flowpilot-agent, GCP:gserviceaccount.comin email)
Code Location: flowpilot-services/persona-api/
Validation Rules:
titlemust be one of the allowed persona titles defined inpersona_config.persona_titlesin the policy manifestcircleis required and must be a non-empty string (represents the community/organization/context)statusmust be one of the allowed statuses defined inpersona_config.persona_statusesin the policy manifestvalid_from&valid_tillmust be valid ISO 8601 timestamps- Custom attributes (e.g.,
autobook_price,autobook_leadtime,autobook_risklevel) are optional and validated according to theattributessection of the policy manifest - Uniqueness: Each user can have multiple personas with the same title, as long as they have different circles (enforced at database level via composite ID: user_sub + title + circle)
Important Notes: The service fails fast at startup if the policy manifest cannot be loaded, ensuring persona configuration is always consistent with authorization policies
Uniqueness Enforcement: Each user can have multiple personas with the same title, as long as they have different circles. The uniqueness constraint is based on the combination of (user_sub, title, circle), enforced at the database level via composite ID. Attempting to create a duplicate persona (same user, title, AND circle) raises a ValueError with HTTP 400, suggesting to use PUT/PATCH for updates instead.
Idempotency Handling: The API is NOT idempotent by design. Duplicate creation attempts fail explicitly, requiring callers to check for existence before creating or catch the error and use update endpoints. This ensures intentional create vs update semantics.
Create
Personas are created via the persona-api POST /v1/personas endpoint.
Request:
POST /v1/personas
Authorization: Bearer <access-token>
Content-Type: application/json
{
"title": "traveler",
"circle": "family",
"valid_from": "2024-01-01T00:00:00Z",
"valid_till": "2026-12-31T23:59:59Z",
"status": "active",
"autobook_consent": true,
"autobook_price": 5000,
"autobook_leadtime": 7,
"autobook_risklevel": 3
}
Response (HTTP 201):
{
"persona_id": "PKbHpCqDnLcNywEo8pev8yQmoU43_traveler_family",
"user_sub": "PKbHpCqDnLcNywEo8pev8yQmoU43",
"title": "traveler",
"circle": "family",
"status": "active",
"valid_from": "2024-01-01T00:00:00Z",
"valid_till": "2026-12-31T23:59:59Z",
"created_at": "2026-01-13T17:00:00Z",
"updated_at": "2026-01-13T17:00:00Z",
"autobook_consent": true,
"autobook_price": 5000,
"autobook_leadtime": 7,
"autobook_risklevel": 3
}
Authorization: The user must be authenticated. The persona is created for the authenticated user (extracted from JWT sub claim).
Uniqueness: If a persona with the same title AND circle already exists for the authenticated user, the API returns HTTP 400 with an error message: "Persona with title '{title}' and circle '{circle}' already exists for this user. Use PATCH/PUT (update) instead of POST (create) to modify it." This explicit failure ensures clear semantics between persona creation and modification. Users can create multiple personas with the same title as long as they have different circles.
Idempotency Handling: The API is NOT idempotent by design. Duplicate creation attempts fail explicitly to: - Prevent accidental expiry modifications without explicit revocation - Ensure callers understand persona lifecycle - Maintain clear audit trail of the persona lifecycle (create → revoke → recreate)
For Idempotent Provisioning: Provisioning scripts should implement their own idempotency logic by checking for existence first (GET /v1/personas) or catching the 400 error and treating it as success if the existing persona matches desired state.
Fetch
Fetch a specific persona by ID:
Authorization: The user must be authenticated.
Update
Modify persona attributes:
PUT /v1/personas/{persona_id}
Authorization: Bearer <access-token>
Content-Type: application/json
{
"autobook_price": 8000,
"autobook_leadtime": 14,
"status": "active"
}
Authorization: The persona must belong to the authenticated user.
List
Users can list their own personas:
Service accounts can list personas for any user:
This endpoint is used by authz-api to fetch persona data for authorization decisions.
All fields are optional (partial update). Only provided fields are updated.
Authorization: The persona must belong to the authenticated user.
Delete
Delete a persona:
Authorization: The persona must belong to the authenticated user.
Authorization Scenarios
Scenario 1: Owner with Traveler Persona
Context:
- Carlo (traveler) creates a workflow
- Carlo executes his own workflow
Authorization Flow:
- Domain-services-api receives request with Carlo's token
- Authz-api extracts
sub=carlo-uuidfrom JWT - Authz-api fetches Carlo's "traveler" persona from persona-api
- OPA evaluates:
authorized_principal: ✓ (owner == principal)persona_valid: ✓ (traveler == traveler)owner_persona_active: ✓ (status == "active")owner_persona_valid_time: ✓ (current time within range)has_consent: ✓ (autobook_consent == true)- Other gates...
- Decision: Allow (if all gates pass)
Scenario 2: Delegated Travel Agent
Context:
- Carlo (traveler) delegates to Yannick (travel-agent)
- Yannick executes Carlo's workflow
Authorization Flow:
- Domain-services-api receives request with Yannick's token
- Authz-api extracts
sub=yannick-uuidfrom JWT - Authz-api queries delegation-api: valid delegation exists
- Authz-api fetches Carlo's "traveler" persona (resource owner)
- OPA evaluates:
authorized_principal: ✓ (valid delegation with "execute" action)persona_valid: ✓ (Yannick's persona "travel-agent" is an agent persona)owner_persona_active: ✓ (Carlo's persona status == "active")owner_persona_valid_time: ✓ (Carlo's persona is valid)has_consent: ✓ (Carlo's autobook_consent == true)- Other gates...
- Decision: Allow (if all gates pass)
Scenario 3: Autonomous AI Agent
Context:
- AI agent attempts to book autonomously (no delegation)
- Carlo's workflow with autobook consent
Authorization Flow:
- AI-agent-api calls domain-services-api with service token
- AuthZEN request:
subject.persona = "ai-agent",context.principal.id = carlo-uuid - Authz-api fetches Carlo's "traveler" persona
- OPA evaluates:
authorized_principal: ✓ (autobook_consent == true, no delegation required)persona_valid: ✓ (context.principal.persona == owner.persona)owner_persona_active: ✓ (status == "active")owner_persona_valid_time: ✓ (within valid time range)has_consent: ✓ (autobook_consent == true)within_cost_limit: Check Carlo's autobook_pricesufficient_advance: Check Carlo's autobook_leadtimeacceptable_risk: Check Carlo's autobook_risklevel
- Decision: Allow or Deny based on ABAC gates
Scenario 4: Persona Mismatch
Context:
- Martine has two personas: "traveler" and "office-manager"
- Martine (office-manager) tries to execute her own workflow created with "traveler" persona
Authorization Flow:
- Workflow was created with
owner.persona = "traveler" - Martine's request has
subject.persona = "office-manager" - OPA evaluates:
authorized_principal: ✓ (owner == principal)persona_valid: ✗ (office-manager ≠ traveler, and office-manager is not allowed for owner execution)
- Decision: Deny with reason_code
"auto_book.persona_mismatch"
Resolution: Martine must switch to her "traveler" persona when executing her own workflows.
Troubleshooting
Persona Not Found
Symptom: Authorization denied with auto_book.no_consent despite user having consent
Causes:
- Persona not created or provisioned
- Persona fetch fails (403, 404, timeout)
- Service account cannot access persona-api
Debug Steps:
-
Verify persona exists:
-
Check authz-api logs for persona fetch errors:
-
Verify service account can access persona-api:
Persona Status Denied
Symptom: Authorization denied with persona.status is inactive or revoked
Causes:
- Persona status is not "active"
- Current time is outside valid_from/valid_till range
Resolution:
-
Update persona status:
-
Update temporal validity:
Firestore Index Missing
Symptom: User-profile-api returns 500 error: "The query requires an index"
Resolution: Create the required composite indexes (see "Storage Backends" section above)
Service Account Authorization Failure
Symptom: User-profile-api returns 403: "Forbidden: Service account required"
Causes:
- Service token doesn't have
client_id=flowpilot-agent(Keycloak) - Service token doesn't have
gserviceaccount.comin email (GCP)
Resolution:
- Local: Use correct Keycloak service account credentials
- GCP: Ensure authz-api uses GCP identity tokens from metadata server
Configuration Management
Persona Configuration in Policy Manifest
Persona configuration is centrally managed in the policy manifest to ensure consistency between authorization policies and profile management.
File: infra/opa/policies/travel/manifest.yaml
Adding New Persona Titles
To add a new persona title:
-
Update the policy manifest (
infra/opa/policies/travel/manifest.yaml):persona_config: persona_titles: - title: traveler description: "End user traveling" can-be-invited: true can-be-delegated-to: false allowed-actions: [read, update] - title: new-persona-title # Add here description: "Description of new persona" can-be-invited: false can-be-delegated-to: true allowed-actions: [read, execute] -
Update OPA policies if the persona requires special authorization logic
-
Regenerate OPA configuration (for local development):
-
Restart services to load new configuration:
Adding New Persona Statuses
To add a new persona status value:
-
Update the policy manifest:
-
Update OPA policies if the status requires special handling
-
Regenerate OPA configuration and restart services (same as above)
Best Practices
- Use descriptive persona titles - Match business roles, not technical identities
- Define all configuration in manifest - Persona titles, statuses, and attributes are defined in
manifest.yaml(single source of truth) - Regenerate OPA config after manifest changes - Run
make generate-opa-configlocally after editing manifest - Never edit
persona_config.jsondirectly - It's auto-generated from manifest.yaml - Set reasonable autobook limits - Start conservative, adjust based on user comfort
- Validate temporal ranges - Ensure valid_till is far enough in the future
- Use descriptive status values - The lifecycle should be clear: pending → active → suspended/inactive/revoked
- Monitor persona status - Implement workflows to revoked/suspend personas when needed
- Limit personas per user - Most users should have 1-2 personas (configurable via
MAX_PERSONAS_PER_USER) - Test delegation with personas - Verify agent personas work correctly with delegations
- Use active personas for authorization - Inactive/suspended personas should not be used in authorization
- Provision test data properly - Use seed scripts to maintain consistency across environments
- Keep manifest in version control - Track all changes to persona configuration
- Handle duplicate creation errors gracefully - The API enforces uniqueness by failing duplicate creates with HTTP 400; client code should check for existence first or catch and handle duplicate errors appropriately
- Implement idempotency in provisioning scripts - Seed scripts catch duplicate creation errors and verify existing state matches desired state, allowing safe re-provisioning
Related Documentation
- Policy Development Guide - How OPA policies use persona data
- Authorization Architecture - Overall authorization flow
- API Reference: Persona API - Full API specification