Skip to main content
POST /queries/run is in beta and limited to organizations in the OpalQuery beta. Contact Opal support to be added.
The POST /queries/run endpoint is the programmatic equivalent of the OpalQuery builder — useful for scripted audits, reporting pipelines, and one-off investigations.

Requirements

See Requirements in the Overview. You’ll also need an Opal API token — see Authentication.

Endpoint

The full schema and an interactive playground are available under API Reference → Opal API in the navigation.

Query types

Every request sets type to one of two modes: Both modes use the same filter vocabulary — AccessEntityFilters to describe entities, and AccessRelationshipFilters to traverse access edges. The Entity filters and Relationship filters sections below document these shared building blocks; the NODE queries and ACCESS_PATH queries sections show how each mode uses them.

Entity filters

An AccessEntityFilters object describes a set of entities by their own properties. It is used in both modes: as nodeFilters / accessFilters.isAccessibleBy / accessFilters.hasAccessTo in NODE queries, and as principalFilter / entitlementFilter / the nested filters inside principalAccessFilters and entitlementAccessFilters in ACCESS_PATH queries. All fields are optional. Multiple sibling fields are combined with AND.

Name match

stringMatchType is one of EQUALS, CONTAINS, STARTS_WITH, ENDS_WITH.

Tag match

key is required. value is optional — omit to match any value for that key. connectionId is optional — set it to restrict to tags sourced from a specific connection.

IDP status match

Matches users by HR/IDP lifecycle status. statuses is one or more of ACTIVE, SUSPENDED, DEPROVISIONED, DELETED, NOT_FOUND, combined with OR. Groups and resources never match this filter, in either polarity. Set not: true to invert within the user domain (e.g. “users whose IDP status is NOT active”).

Admin owner match

Matches resources and groups by admin owner. ownerIDs is a list of owner (group) UUIDs; an entity matches if its admin owner is any of them. User entities never match this filter, in either polarity. Set not: true to return resources/groups not owned by the given owners.

Logical composition

allOf, anyOf, and not each take the same shape as the outer filter and can be nested arbitrarily deep.
Resources tagged env=prod and team=platform, excluding AWS IAM roles.

Relationship filters

An AccessRelationshipFilters object traverses access edges. It is used in both modes: as accessFilters in NODE queries to narrow returned entities by their access relationships, and as principalAccessFilters / entitlementAccessFilters in ACCESS_PATH queries to scope one side of the path by its own access edges. Both fields take an AccessEntityFilters object. Both direct and indirect (through nested group memberships) edges are considered. When both are set, both must be satisfied. hasAccessTo additionally accepts two fields that constrain the role on the access edge: These are only honored inside hasAccessTo — they have no effect in isAccessibleBy or entity filters.

NODE queries

A NODE query returns a paginated list of entities (users, resources, or groups) that match your filters.

Request

At least one of nodeFilters or accessFilters is required.

Response

node.id is the entity’s Opal UUID — pass it to other API endpoints (e.g. GET /resources/{resource_id}) to fetch full details.

Examples

All users
AWS IAM roles tagged env=prod, accessible by contractors Combines entityItemTypes, entityTag, and accessFilters.
Users with write access to a specific GitHub repo Uses roleRemoteIds inside hasAccessTo to filter by role.
Resources owned by a specific admin, accessible by suspended users (deprovisioning gap) Combines entityAdminOwner and hrIdpStatus.
Full curl request

ACCESS_PATH queries

An ACCESS_PATH query returns paginated access paths — each result is one principal → entitlement pair, the terminal access level, the depth, and the chain of entity IDs in between.

Request

At least one of principalFilter or entitlementFilter is required.

Edge filter

  • directOnly — when true, return only direct (depth-1) principal → entitlement edges.
  • accessDurationTypeEXPIRING_ONLY or PERMANENT_ONLY, to constrain by whether the terminal access expires.

Advanced access filters

principalAccessFilters and entitlementAccessFilters scope one side of the path by its own access edges, using the same AccessRelationshipFilters shape as NODE accessFilters:
  • principalAccessFilters.hasAccessTo — keep only principals that also have access to a matching entity.
  • principalAccessFilters.isAccessibleBy — keep only principals that are also accessible by a matching entity.
  • entitlementAccessFilters — the same, applied to the entitlement side.
Each access filter only takes effect when that side also carries an entity filter: principalAccessFilters requires principalFilter, and entitlementAccessFilters requires entitlementFilter. Supplied without a matching entity filter, it is silently ignored.

Response

  • depth — number of hops from principal to entitlement (1 = direct).
  • path — entity IDs along the chain from principal to entitlement.
  • totalCount — exact count of matching paths; only populated when includeCount: true was set on the request.

Examples

All access paths for a specific user
Separation of duties — users who can reach billing-prod and also hold admin access Uses principalAccessFilters.hasAccessTo to scope principals to those with an additional access relationship.
Deprovisioning gap — direct access still held by suspended users Combines hrIdpStatus on the principal side with directOnly on the edge.

Pagination

Both query types use cursor-based pagination. Set first to control the page size (default 200). When pageInfo.hasNextPage is true, pass pageInfo.endCursor as after on the next request.
For ACCESS_PATH queries, omit includeCount on pages after the first — the total count is stable and recomputing it on every page is expensive.
Last modified on July 30, 2026