WorkOS Docs Homepage
FGA
API referenceDashboardSign In
Getting StartedOverviewOverviewQuick StartQuick StartPlaygroundPlaygroundKey ConceptsSchemaSchemaWarrantsWarrantsResourcesResourcesPoliciesPoliciesQuery LanguageQuery LanguageWarrant TokensWarrant TokensOperations & UsageOperations & UsageManagementSchema ManagementSchema ManagementLocal DevelopmentLocal DevelopmentIdentity Provider SessionsIdentity Provider SessionsModelingOrg Roles & PermissionsOrg Roles & PermissionsCustom RolesCustom RolesGoogle DocsGoogle DocsEntitlementsEntitlementsUser GroupsUser GroupsManaged Service ProviderManaged Service ProviderAttribute-Based Access ControlAttribute-Based Access ControlConditional RolesConditional RolesPolicy ContextPolicy ContextPublic AccessPublic AccessSuperusersSuperusersBlocklistsBlocklists
API Reference
API Reference
Events
Events
Integrations
Integrations
Migrate to WorkOS
Migrate to WorkOS
SDKs
SDKs

Policies

Use policies to implement advanced attribute based access control with FGA, providing relevant data from your application at access control check time.

On this page

  • Warrant Policies
    • Context
  • Schema Policies
    • Defining and Using Policies
    • Attribute-Based Access Control (ABAC)
    • Combining Policies with ReBAC
  • Managing Policies via API
  • Making Checks
  • Policies in Schema JSON
  • Advanced Usage
    • Injected Context
    • Helper Functions
    • Combine with Inheritance Rules
  • Passing Context vs. Injecting Context
  • Common Use Cases
  • Next Steps

WorkOS FGA allows you to define custom logic that is executed when evaluating access checks. A policy is a boolean expression that specifies additional conditions to be satisfied in order for an access check to be authorized. Use policies to enforce complex rules and conditions that go beyond simple role-based access control (RBAC) or attribute-based access control (ABAC).

Policies can be defined on warrants or as part of your schema.

FGA currently supports defining policy expressions using expr. Support for more policy languages will be coming soon.

Warrant Policies

You can optionally include a policy in a warrant. For a warrant to match a check/query, its policy must evaluate to true. The system evaluates policies after matching the warrant based on its resource, relation, and subject attributes. It evaluates the policy in the context of the check/query request, using any dynamic values provided via the context attribute (see context below) to process the expression.

For example, the following warrant states that [role:accountant] is a [member] of [permission:view-profits-and-losses] only when companyId == 'wayne-enterprises':

{
"resource_type": "permission",
"resource_id": "view-profits-and-losses",
"relation": "member",
"subject": {
"resource_type": "role",
"resource_id": "accountant"
},
"policy": "companyId == 'wayne-enterprises'"
}

Policies can reference dynamic variables. You must provide values for these variables in check or query requests via the context attribute (e.g., role, tenant, or geographic location). Before evaluating a policy, the system substitutes the provided values into the expression. Policy expressions undergo static type checking, so type mismatches prevent evaluation from returning true. Policies with missing values or evaluation errors also do not return true. The system compiles and statically checks policies for errors upon creation.

Policies have numerous uses but are most commonly used to implement forms of attribute-based access control (ABAC). For example, create a warrant that only matches users visiting from a specific IP address:

{
"resource_type": "database",
"resource_id": "prod",
"relation": "admin",
"subject": {
"resource_type": "user",
"resource_id": "ops-user"
},
"policy": "user.client_ip == '192.168.1.1'"
}

Combine policies with role-based access control (RBAC) to support different role/permission mappings per customer or tenant. For example, define a warrant stating that [role:accountant] grants [permission:view-balance-sheet] only when companyId == 'wayne-enterprises':

{
"resource_type": "permission",
"resource_id": "view-balance-sheet",
"relation": "member",
"subject": {
"resource_type": "role",
"resource_id": "accountant"
},
"policy": "companyId == 'wayne-enterprises'"
}

Create another warrant specifying that [role:accountant] grants users [permission:view-profits-and-losses] only when companyId == 'daily-planet':

{
"resource_type": "permission",
"resource_id": "view-profits-and-losses",
"relation": "member",
"subject": {
"resource_type": "role",
"resource_id": "accountant"
},
"policy": "companyId == 'daily-planet'"
}

Context

Make access checks, passing in different companyId values via the context based on the company a user belongs to:

{
"checks": [
{
"resource_type": "permission",
"resource_id": "view-profits-and-losses",
"relation": "member",
"subject": {
"resource_type": "role",
"resource_id": "accountant"
},
"context": {
"companyId": "wayne-enterprises"
}
}
]
}

This access check returns false because [role:accountant] only grants [permission:view-profits-and-losses] within the context of company daily-planet.

Schema Policies

You can reference policies in your schema, allowing you to define and reuse complex rules across different relations and inheritance rules. Use policies in the inherit clause of your schema definition. For example, consider the following schema:

version 0.3
type user
type organization
relation viewer [user]
relation view []
inherit view if
all_of
policy ip_allowed
relation viewer
policy ip_allowed(clientIp string) {
clientIp matches "192\\.168\\..*\\..*"
}

Here, the view relation is inherited based on the ip_allowed policy and the viewer relation. This means that users must meet the conditions of the ip_allowed policy and also be in the viewer relation to access the view permission.

view has no allowed types, so it cannot be assigned a warrant directly. Instead, it inherits from the viewer relation and the ip_allowed policy so that we can check if a user is in the viewer relation and also meets the conditions of the ip_allowed policy.

Make sure schema version is set to 0.3 or higher to use policies in your schema.

Defining and Using Policies

Policies can be defined in the Schema Editor of the WorkOS FGA Dashboard or via the API. Each policy consists of the following fields:

  • name – a unique identifier for the policy
  • language – currently only expr is supported
  • parameters – define which values the policy accepts
  • expression – the boolean expression that defines the policy

Policy Syntax

Expr policies are defined directly in your FGA schema, making it easy to view policies alongside your inheritance rules. More languages will be supported in the future and managed through a different user interface.

policy <policy_name>(<parameter_name> <parameter_type>, ...) {
<expression>
}

View expr language documentation.

Policies are referenced in inheritance rules using their name.

inherit <relation_name> if
policy <policy_name>

Attribute-Based Access Control (ABAC)

You can use policies to implement pure attribute-based access control (ABAC) without any inheritance rules or warrant data. This allows you to define access control based solely on user attributes or other context values as you would with a policy engine. For example:

version 0.3
type user
type organization
relation view_internal_settings []
inherit view_internal_settings if
policy staff_user
policy staff_user(user map) {
user.email endsWith "@internal-domain.com" && user.role == "staff"
}

In this example, the view_internal_settings relation inherits from the staff_user policy. This means only users with a staff role and an email ending in @internal-domain.com can access view_internal_settings.

Since this policy is the sole inheritance rule for view_internal_settings, FGA does not check for warrants when evaluating access. This allows you to use FGA purely as an attribute-based access control (ABAC) system if desired.

Combining Policies with ReBAC

Policies can also be combined with ReBAC inheritance rules to create more complex access control models. Consider the following example:

version 0.3
type user
type organization
relation admin [user]
relation configure_payments [user]
inherit configure_payments if
all_of
relation admin
policy has_strong_auth
policy has_strong_auth(user_attributes map) {
user_attributes.mfa_enabled == true &&
user_attributes.account_age_days > 30
}

In this example, the configure_payments relation inherits from both the admin relation and the has_strong_auth policy. This means that users must be an admin and meet the strong authentication requirements to access the configure_payments relation.

Managing Policies via API

WorkOS FGA provides API endpoints if you prefer managing policies programmatically and separately from the schema.

For more details, see the Policies API documentation.

Making Checks

When making an FGA check, pass the required context values as you would with warrant policies. FGA evaluates warrant and schema policies together during access checks.

{
"checks": [
{
"resource_type": "organization",
"resource_id": "acme-corp",
"relation": "configure_payments",
"subject": {
"resource_type": "user",
"resource_id": "123"
},
"context": {
"user_attributes": { "mfa_enabled": true, "account_age_days": 45 }
}
}
]
}

Policies in Schema JSON

Policies can also be defined in the schema JSON format. Here’s an example of how to define a policy in JSON:

{
"version": "0.3",
"resource_types": {
"user": {},
"organization": {
"relations": {
"view": {
"policy": "ip_allowed"
}
}
}
},
"policies": {
"ip_allowed": {
"name": "ip_allowed",
"language": "expr",
"parameters": [
{
"name": "clientIp",
"type": "string"
}
],
"expression": "clientIp matches \"192\\\\.168\\\\..*\\\\..*\""
}
}
}

At runtime, if a policy fails to evaluate due to an invalid or missing context parameter, the system will return a 400 Bad Request in response to the check or query.

Advanced Usage

Injected Context

Policies can reference dynamic variables that are injected by the FGA system at runtime. When a policy is evaluated, the system substitutes the provided values into the expression. This allows you to create policies that depend on runtime context, such as warrant data that the policy is stored with or check arguments.

check_ctx

A map containing the subject, resource, and relation of the check (or sub-check) executing the policy. This context variable is only available when the policy is evaluated in the context of a check (otherwise it is an empty map).

check_ctx
{
"subject_type": "user",
"subject_id": "123",
"relation": "view_feature_1",
"resource_type": "organization",
"resource_id": "acme-corp"
}
policy is_user_in_org(user_attributes map) {
check_ctx.resource_type == "organization" && user_attributes.organization_id == check_ctx.resource_id
}

Use check_ctx in a policy when:

  • You want to avoid duplicating check arguments in context
    Instead of manually passing subject_id, resource_type, or relation as context values in every access check, reference them directly via check_ctx to reduce redundancy and simplify your check requests.

  • Your policy logic needs to vary based on the check’s subject or resource
    For example, use check_ctx when applying different rules depending on whether the subject is a user or service, or if the resource type is document versus organization.

  • You’re leveraging policy helper functions
    Pass resource ids from check_ctx into helper functions.

warrant_ctx

A map containing the subject, resource, and relation of the warrant that the policy was stored on. This context variable is only available when the policy is stored on a warrant (otherwise it is an empty map).

warrant_ctx
{
"subject_type": "user",
"subject_id": "123",
"relation": "editor",
"resource_type": "organization",
"resource_id": "acme-corp",
"created_at": "2023-10-01T00:00:00Z"
}
policy warrant_not_expired() {
let expiration = duration("1h");
date(warrant_ctx.created_at) > now() + expiration
}

Use warrant_ctx in a policy when:

  • You need time-based or expiring access control
    Reference warrant_ctx.created_at to enforce temporal constraints like short-lived or trial permissions.

  • Your policy behavior depends on the warrant’s subject, resource, or relation
    For example, restrict logic to apply only if the warrant’s relation is "editor" or resource_type is "project".

  • You need to evaluate policies only within warrant-based contexts
    Helps enforce logic that should not apply in schema-only (inheritance) scenarios.

  • You’re using policy helper functions
    Pass resource ids from warrant_ctx into helper functions.

Context variables can be empty if the policy is not evaluated in the context of a check or warrant. Make sure to check for empty values in your policy expressions to avoid errors.

Helper Functions

In addition to all of the built-in functions available in the expr language, FGA provides the following helper functions for use in policies:

get_metadata

Fetches metadata for a given resource type and id when a policy is evaluated. This allows you to access metadata attributes stored on the resource in FGA without having to pass them in as context. This is especially useful when you don’t want to update your check requests to include additional context values after schema changes.

policy user_in_org() {
let subject_metadata = get_metadata(check_ctx.subject_type, check_ctx.subject_id);
subject_metadata.organization_id == check_ctx.resource_id &&
check_ctx.subject_type == "user" &&
check_ctx.resource_type == "organization"
}

Make sure to check for empty values in your policy expressions to avoid errors. It is best practice to avoid nested keys or use optional chaining to prevent errors when accessing metadata attributes.

jwt_claim

Retrieves a specific claim from the JWT used to authenticate an access check. This is useful for policy logic that depends on user attributes embedded in the JWT, without needing to explicitly pass them through the context. This helper returns nil if a different authentication method (i.e. not a JWT) was used or the claim does not exist.

This also works directly with Custom Attributes from AuthKit JWT templates, allowing you to access user attributes directly in your policies.

Ensure your JWKS (JSON Web Key Set) is properly configured to validate JWT signatures and authorize requests.

policy user_is_workos_admin() {
let role = jwt_claim("role");
let email = jwt_claim("email");
role == "admin" &&
check_ctx.subject_type == "user" &&
email endsWith "@workos.com"
}

Combine with Inheritance Rules

Combine policies with inheritance rules to create complex access control models. For example, define a policy that checks specific conditions and apply it across multiple relations or inheritance rules:

version 0.3
type user
type staff_group
relation member [user]
type org_role
relation member [user]
type organization
relation internal_admin [staff_group]
relation viewer [user, org_role]
relation view_feature_1 []
inherit view_feature_1 if
any_of
relation member on internal_admin [staff_group]
all_of
any_of
relation viewer
relation member on viewer [org_role]
policy valid_enterprise_plan
policy valid_enterprise_plan(payment_plan map) {
payment_plan.is_active == true && payment_plan.tier == "enterprise"
}

In this example:

  • view_feature_1 access can be inherited based on multiple conditions.
  • Internal admins (staff_group members) can access view_feature_1.
  • Users in viewer or an org_role can also access it if they meet the valid_payment_plan policy requirements.

Passing Context vs. Injecting Context

When using policies, you can provide context in two ways: by passing values directly in the check request, or by injecting them into the policy using get_metadata. The right approach depends on where your data lives, how often it changes, and how you want to manage changes to your schema or policies.

Pass context: Use this method when you want to provide specific values for the policy to evaluate. This is useful for dynamic values that may change frequently or are specific to the check being made.

Passing context also does not require syncing data between your application and FGA, as the context is provided at check time. The major drawback is that it can lead to large check requests if you have many attributes to pass in. This can also make it difficult to manage and maintain the context values over time since changes to your schema or policies may require updates to the context values in your check requests.

Inject context: Use this method when you want to fetch metadata or other attributes from the resource itself. This is useful for static values that are stored in FGA and do not change frequently or when you want to avoid passing large amounts of context data in the check request.

Schema or policy changes do not require updates to the context values in your check requests, as the metadata is fetched at runtime. The major drawback is that it requires syncing data between your application and FGA.

See the Policy Context modeling guide for more details on how to use context in your policies.

Common Use Cases

Using policies and inheritance rules together provides a powerful way to model permissions for:

  • Entitlements (e.g., feature access based on plan level)
  • Feature flags (e.g., enabling experimental features for specific groups)
  • Domain-specific logic (e.g., enforcing security constraints specific data attributes)
  • Temporal data (e.g., granting temporary access based on time-based or location-based policies)

Next Steps

To start using policies in your schema, ensure the following:

  1. Create policies in the schema or with the Policy API
  2. Reference policies in your schema
  3. Pass the correct context values when making access checks
  4. Test your schema logic using the FGA dashboard and API
Query LanguageQuery which resources users have access to in your application
Up next
© WorkOS, Inc.
FeaturesAuthKitSingle Sign-OnDirectory SyncAdmin PortalFine-Grained Authorization
DevelopersDocumentationChangelogAPI Status
ResourcesBlogPodcastPricingSecuritySupport
CompanyAboutCustomersCareersLegalPrivacy
© WorkOS, Inc.