Help us learn about your current experience with the documentation. Take the survey.

Policy store API

  • Tier: Ultimate
  • Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated
  • Status: Experiment

This feature is an experiment. The endpoints can change without notice.

Use this API to author security policies in the policy store. A policy belongs to an organization, responds to a single trigger, and carries the rules and actions that make up its behavior.

These endpoints are available only when all of the following are true:

  • The security_policies_v2 feature flag is enabled.
  • An administrator has enabled the policy store experiment for the instance in Admin > Settings > Security and compliance.
  • The organization has opted in through its policy_store_experiment_enabled organization setting, set through the policyStoreExperimentEnabled argument of the organizationUpdate GraphQL mutation.

When any of these is not true, the endpoints return 404 Not Found. When the instance is not licensed for security orchestration policies, they return 403 Forbidden.

Catalogs

The catalog endpoints describe what a policy can be built from, and return the same static content to every eligible caller. The caller must be an authenticated user whose organization has the security_policies_v2 feature flag enabled. When this is not true, the endpoints return 404 Not Found.

List all triggers

List all triggers a policy can respond to.

GET /security/policy_store/triggers

If successful, returns 200 and the following response attributes:

AttributeTypeDescription
[].idstringID of the trigger, used as trigger_type when authoring a policy.
[].namestringDisplay name of the trigger.

Example request:

curl --request GET \
  --url "https://gitlab.example.com/api/v4/security/policy_store/triggers"

Example response:

[
  { "id": "deployment_requested", "name": "Deployment requested" },
  { "id": "environment_advanced", "name": "Environment advanced" },
  { "id": "deployment_promoted", "name": "Deployment promoted" }
]

List all actions

List all actions a policy can take.

GET /security/policy_store/actions

If successful, returns 200 and the following response attributes:

AttributeTypeDescription
[].idstringID of the action.
[].namestringDisplay name of the action.

Example request:

curl --request GET \
  --url "https://gitlab.example.com/api/v4/security/policy_store/actions"

Example response:

[
  { "id": "block", "name": "Block" },
  { "id": "require_approval", "name": "Require approval" }
]

List all rule kinds

List all rule kinds a policy can be built from.

GET /security/policy_store/rules

If successful, returns 200 and the following response attributes:

AttributeTypeDescription
[].idstringID of the rule kind.
[].namestringDisplay name of the rule kind.

Example request:

curl --request GET \
  --url "https://gitlab.example.com/api/v4/security/policy_store/rules"

Example response:

[
  { "id": "custom", "name": "Custom" },
  { "id": "calendar", "name": "Calendar" },
  { "id": "environment", "name": "Environment" }
]

Policies

Every call to a policy endpoint must be authenticated, and the caller must be an owner of the organization or an instance administrator. A caller who cannot administer the organization receives 403 Forbidden, and one who cannot see the organization at all receives 404 Not Found.

A policy that belongs to another organization is indistinguishable from one that does not exist. An ID cannot be used to read or change a policy across organizations.

Policy scope

A policy applies everywhere unless it carries a scope. A scope is authored one of two ways, and a request may use one or the other but not both:

  • policy_scope: structured data that GitLab compiles into scope_rego.
  • scope_rego: a Rego program supplied directly, stored as authored.

A request that supplies both returns 400 Bad Request. An empty scope_rego does not count as the second form, so either operation accepts it alongside policy_scope. On Create a policy, an empty value has the same effect as omitting it. On Update a policy, it retires an authored program and compiles a new one from policy_scope.

scope_rego is always present in a response, because a policy with no scope compiles to a program that applies to every project. policy_scope is null when the Rego was authored directly, because a hand-written program has no structured form.

scope_dimensions lists the dotted context paths, such as compliance_frameworks or project.id, that scope_rego reads to decide whether the policy applies. GitLab derives this list, so it ignores any value you send for the attribute. This value is always an array, empty when the policy is unscoped, unless scope_rego was authored directly instead of compiled from policy_scope. In that case, GitLab cannot derive the paths from a hand-written program, so scope_dimensions is null, meaning the paths are not known rather than empty.

Policy scope structure

policy_scope holds one or more criteria, and match_mode controls how they combine. A criterion names IDs, either as integers or as objects with an id key. GitLab deduplicates and sorts the IDs, so authoring order does not change the compiled program.

AttributeTypeDescription
applicationobjectincluding and excluding lists of application security attribute IDs.
business_impactobjectincluding and excluding lists of business impact security attribute IDs.
business_unitobjectincluding and excluding lists of business unit security attribute IDs.
compliance_frameworksarrayIDs of compliance frameworks the project must carry. Takes a list directly, rather than including and excluding.
exposureobjectincluding and excluding lists of exposure security attribute IDs.
groupsobjectincluding and excluding lists of group IDs.
match_modestringEither all or any. With all, every criterion must match. With any, one match is enough. Any other value is treated as all.
projectsobjectincluding and excluding lists of project IDs. excluding also accepts {"type": "personal"} and {"type": "archived"}, which exclude every project of that kind.

For example:

{
  "match_mode": "any",
  "compliance_frameworks": [{ "id": 5 }],
  "projects": { "including": [12, 34], "excluding": [{ "type": "archived" }] },
  "groups": { "including": [{ "id": 7 }] }
}

A value GitLab cannot read as an ID returns 400 Bad Request. That covers a value that is not a number, and a number outside the range 1 to 9223372036854775807.

Three cases are accepted and worth knowing, because each one scopes the policy differently from what you might expect:

  • An including list that names no IDs matches nothing for that criterion. Under match_mode: all the policy then applies to no project. Under match_mode: any another criterion can still match.
  • An excluding list that names no IDs excludes nothing.
  • A criterion GitLab does not recognize has no effect. If it was the only criterion supplied, the policy applies to every project.

Rules and actions

rules and actions are arrays. Each entry has the following attributes:

AttributeTypeRequiredDescription
typestringYesFor a rule, one of the IDs returned by List all rule kinds. For an action, one of the IDs returned by List all actions.
valuestring or hashNoWhat the entry acts on. A custom rule takes Rego source as a string. A calendar or environment rule takes a hash, as does every action.

An entry cannot be blank. A blank entry returns 400 Bad Request, and the error names each blank position, for example rules[0] is blank.

Each array accepts at most 5 entries, and each entry cannot serialize to more than 4096 bytes. Exceeding either limit returns 400 Bad Request. An oversized entry names each offending position, for example rules has an entry exceeding maximum size of 4096 bytes at 0.

A request replaces the whole array. You cannot add or remove a single entry.

Send rules and actions as JSON with a Content-Type: application/json header. A form-encoded body can carry both arrays, but every value in one arrives as a string, so a value that is not a string cannot be expressed that way.

Response attributes

The policy endpoints return the following attributes:

AttributeTypeDescription
actionsarrayActions the policy takes.
created_atstringDate and time the policy was created.
descriptionstringDescription of the policy.
idintegerID of the policy.
lifecycle_statestringEither active or disabled.
modestringOne of audit, warn, or enforce.
namestringName of the policy.
namespace_idintegerID of the group that owns the policy. Always null today, because no endpoint accepts a namespace_id attribute, so every policy created through this API is owned by its organization.
organization_idintegerID of the organization the policy belongs to.
policy_regostringThe policy’s rules, compiled to a single Rego module. null for a policy with no rules.
policy_scopeobjectStructured scope of the policy, or null when the Rego was authored directly.
rulesarrayRules of the policy.
scope_dimensionsarrayDotted context paths scope_rego reads to decide whether the policy applies. GitLab derives this value, so it ignores any value you send for it. null when scope_rego was authored directly, otherwise an array, empty when the policy is unscoped.
scope_regostringCompiled scope of the policy, as Rego.
trigger_typestringTrigger the policy responds to.
updated_atstringDate and time the policy was last changed.
versionintegerRevision of the policy. An update that changes at least one value raises it by one.

List all policies

List all policies belonging to an organization.

GET /organizations/:id/security/policy_store

Supported attributes:

AttributeTypeRequiredDescription
idintegerYesID of the organization.
pageintegerNoPage of results to return. Defaults to 1.
per_pageintegerNoNumber of results per page. Defaults to 20, and any value above 100 is capped at 100.
trigger_typestringNoReturn only the policies that respond to this trigger. One of the IDs returned by List all triggers.

This endpoint returns paginated results. For performance reasons, it never returns the x-total or x-total-pages headers or the rel="last" link, regardless of how many policies the organization has: check x-next-page to find out whether another page follows.

If successful, returns 200 and an array of policy attributes.

Example request:

curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" \
  --url "https://gitlab.example.com/api/v4/organizations/1/security/policy_store?per_page=20"

Example response:

[
  {
    "id": 1,
    "organization_id": 1,
    "namespace_id": null,
    "name": "Block deployments on critical findings",
    "description": null,
    "version": 1,
    "trigger_type": "deployment_requested",
    "rules": [{ "type": "custom", "value": "package governance" }],
    "policy_rego": "package governance\n",
    "actions": [{ "type": "block" }],
    "policy_scope": null,
    "scope_dimensions": [],
    "scope_rego": "package gitlab.scope\n\napplicable := [result.policy | some result in results; result.applies]\n...",
    "mode": "enforce",
    "lifecycle_state": "active",
    "created_at": "2026-08-07T13:56:32.985Z",
    "updated_at": "2026-08-07T13:56:32.985Z"
  }
]

Retrieve a policy

Retrieve a single policy from an organization.

GET /organizations/:id/security/policy_store/:policy_id

Supported attributes:

AttributeTypeRequiredDescription
idintegerYesID of the organization.
policy_idintegerYesID of the policy.

If successful, returns 200 and the policy attributes.

Example request:

curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" \
  --url "https://gitlab.example.com/api/v4/organizations/1/security/policy_store/1"

Example response:

{
  "id": 1,
  "organization_id": 1,
  "namespace_id": null,
  "name": "Block deployments on critical findings",
  "description": null,
  "version": 1,
  "trigger_type": "deployment_requested",
  "rules": [{ "type": "custom", "value": "package governance" }],
  "policy_rego": "package governance\n",
  "actions": [{ "type": "block" }],
  "policy_scope": null,
  "scope_dimensions": [],
  "scope_rego": "package gitlab.scope\n\napplicable := [result.policy | some result in results; result.applies]\n...",
  "mode": "enforce",
  "lifecycle_state": "active",
  "created_at": "2026-08-07T13:56:32.985Z",
  "updated_at": "2026-08-07T13:56:32.985Z"
}

Create a policy

Create a policy in an organization.

POST /organizations/:id/security/policy_store

Supported attributes:

AttributeTypeRequiredDescription
idintegerYesID of the organization.
namestringYesName of the policy. Maximum 255 characters. Must be unique in the organization.
rulesarrayYesRules of the policy. At least one entry is required, up to 5. Each entry must serialize to at most 4096 bytes. Rejected when the entries compile to a Rego module larger than 65536 bytes. That module is returned as policy_rego.
trigger_typestringYesTrigger the policy responds to. One of the IDs returned by List all triggers.
actionsarrayNoActions the policy takes. Up to 5 entries. Each entry must serialize to at most 4096 bytes.
descriptionstringNoDescription of the policy. Maximum 4096 characters.
lifecycle_statestringNoEither active or disabled. Defaults to active.
modestringNoOne of audit, warn, or enforce. Defaults to enforce.
policy_scopeobjectNoStructured scope of the policy. Cannot be combined with a non-empty scope_rego. Rejected when it compiles to more than 4096 characters of Rego.
scope_regostringNoScope of the policy, authored as Rego. Maximum 4096 characters. A non-empty value cannot be combined with policy_scope.

If successful, returns 201 and the policy attributes. The following conditions return 400 Bad Request:

  • An attribute is invalid.
  • Both scope forms are supplied.
  • The name is already taken in the organization.
  • A compiled scope_rego exceeds 4096 characters.
  • The rules compile to more than 65536 bytes of Rego.
  • rules or actions carries more than 5 entries.
  • An entry in rules or actions serializes to more than 4096 bytes.

Example request:

curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "Block deployments on critical findings",
    "trigger_type": "deployment_requested",
    "rules": [{ "type": "custom", "value": "package governance" }],
    "actions": [{ "type": "block" }],
    "policy_scope": { "compliance_frameworks": [{ "id": 5 }] }
  }' \
  --url "https://gitlab.example.com/api/v4/organizations/1/security/policy_store"

Example response:

{
  "id": 1,
  "organization_id": 1,
  "namespace_id": null,
  "name": "Block deployments on critical findings",
  "description": null,
  "version": 1,
  "trigger_type": "deployment_requested",
  "rules": [{ "type": "custom", "value": "package governance" }],
  "policy_rego": "package governance\n",
  "actions": [{ "type": "block" }],
  "policy_scope": { "compliance_frameworks": [{ "id": 5 }] },
  "scope_dimensions": ["compliance_frameworks"],
  "scope_rego": "package gitlab.scope\n\napplicable := [result.policy | some result in results; result.applies]\n...",
  "mode": "enforce",
  "lifecycle_state": "active",
  "created_at": "2026-08-07T13:56:32.985Z",
  "updated_at": "2026-08-07T13:56:32.985Z"
}

Update a policy

Update a policy in an organization. Every attribute other than the path parameters is optional, but a request must name at least one. Attributes that are not sent are left as they are, and an update that changes at least one value raises version by one. A request that restates the stored values changes nothing, and leaves version as it is.

PATCH /organizations/:id/security/policy_store/:policy_id

Supported attributes:

AttributeTypeRequiredDescription
idintegerYesID of the organization.
policy_idintegerYesID of the policy.
actionsarrayNoActions the policy takes. Replaces the stored actions, up to 5 entries. Each entry must serialize to at most 4096 bytes.
descriptionstringNoDescription of the policy. Maximum 4096 characters.
lifecycle_statestringNoEither active or disabled.
modestringNoOne of audit, warn, or enforce.
namestringNoName of the policy. Maximum 255 characters. Must be unique in the organization.
policy_scopeobjectNoStructured scope of the policy. Cannot be combined with a non-empty scope_rego. Rejected when it compiles to more than 4096 characters of Rego.
rulesarrayNoRules of the policy. Replaces the stored rules, up to 5 entries. Each entry must serialize to at most 4096 bytes. Rejected when the entries compile to a Rego module larger than 65536 bytes. That module is returned as policy_rego.
scope_regostringNoScope of the policy, authored as Rego. Maximum 4096 characters. Send an empty value to retire an authored program and recompile from policy_scope.
trigger_typestringNoTrigger the policy responds to. One of the IDs returned by List all triggers.

When you rename a policy, GitLab must recompile a generated scope_rego, because the policy name appears in the generated program. A scope_rego that was authored directly is left as it is.

If successful, returns 200 and the policy attributes. The following conditions return 400 Bad Request:

  • No attribute to change is supplied.
  • An attribute is invalid.
  • Both scope forms are supplied.
  • The new name is already taken in the organization.
  • A recompiled scope_rego exceeds 4096 characters.
  • The replacement rules compile to more than 65536 bytes of Rego.
  • The replacement rules or actions carries more than 5 entries.
  • An entry in the replacement rules or actions serializes to more than 4096 bytes.

Example request:

curl --request PATCH --header "PRIVATE-TOKEN: <your_access_token>" \
  --data-urlencode "name=Renamed policy" \
  --url "https://gitlab.example.com/api/v4/organizations/1/security/policy_store/1"

Example response:

{
  "id": 1,
  "organization_id": 1,
  "namespace_id": null,
  "name": "Renamed policy",
  "description": null,
  "version": 2,
  "trigger_type": "deployment_requested",
  "rules": [{ "type": "custom", "value": "package governance" }],
  "policy_rego": "package governance\n",
  "actions": [{ "type": "block" }],
  "policy_scope": null,
  "scope_dimensions": [],
  "scope_rego": "package gitlab.scope\n\napplicable := [result.policy | some result in results; result.applies]\n...",
  "mode": "enforce",
  "lifecycle_state": "active",
  "created_at": "2026-08-07T13:56:32.985Z",
  "updated_at": "2026-08-07T14:02:47.198Z"
}

Delete a policy

Delete a policy from an organization.

DELETE /organizations/:id/security/policy_store/:policy_id

Supported attributes:

AttributeTypeRequiredDescription
idintegerYesID of the organization.
policy_idintegerYesID of the policy.

If successful, returns 204 and an empty response body.

Example request:

curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" \
  --url "https://gitlab.example.com/api/v4/organizations/1/security/policy_store/1"