> ## Documentation Index
> Fetch the complete documentation index at: https://docs.opal.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Query API

> Run ad-hoc OpalQuery requests over HTTP to find entities and traverse access relationships.

The `POST /queries/run` endpoint runs an ad-hoc OpalQuery and returns matched entities. It is the programmatic equivalent of the [OpalQuery builder](/docs/opal-query) in the dashboard — useful for scripted audits, reporting pipelines, and one-off investigations.

<Info>
  `POST /queries/run` is in beta and limited to organizations in the OpalQuery beta. Contact Opal support to be added.
</Info>

## Requirements

See [Requirements](/docs/opal-query#requirements) in the Overview. You'll also need an Opal API token — see [Authentication](/reference/authentication).

## Endpoint

```http theme={null}
POST https://api.opal.dev/v1/queries/run
Authorization: Bearer <YOUR_API_TOKEN>
Content-Type: application/json
```

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

## Request anatomy

Every request has the same outer shape. Only `NODE` queries are supported today — they return entities (users, resources, or groups) that match your filters.

```json theme={null}
{
  "type": "NODE",
  "query": {
    "nodeFilters": { /* AccessEntityFilters — narrow which entities to return */ },
    "accessFilters": { /* AccessRelationshipFilters — traverse access edges (optional) */ }
  },
  "first": 200,
  "after": "<cursor from a previous response>"
}
```

| Field   | Type    | Description                                                                               |
| ------- | ------- | ----------------------------------------------------------------------------------------- |
| `type`  | string  | Discriminator. Must be `NODE`.                                                            |
| `query` | object  | The filter body. See [Node filters](#node-filters) and [Access filters](#access-filters). |
| `first` | integer | Maximum results to return. Defaults to `200`.                                             |
| `after` | string  | Pagination cursor from a previous response's `pageInfo.endCursor`.                        |

`nodeFilters` narrows the set of entities returned. `accessFilters` (optional) further restricts the set to entities connected by an access edge to entities matching another filter — for example, "resources accessible by contractors."

## Node filters

`nodeFilters` is an `AccessEntityFilters` object. All fields are optional, and multiple sibling fields are combined with **AND**.

| Field             | Type      | Description                                                                                                                                  |
| ----------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `entityTypes`     | string\[] | Top-level type. One or more of `USER`, `RESOURCE`, `GROUP`.                                                                                  |
| `entityItemTypes` | enum\[]   | Granular subtype. E.g. `AWS_IAM_ROLE`, `OKTA_GROUP`, `GIT_HUB_REPO`. The full list is enumerated in the OpenAPI spec (`EntityItemTypeEnum`). |
| `entityName`      | object    | Match by display name. See [Name match](#name-match).                                                                                        |
| `entityTag`       | object    | Match by tag key/value. See [Tag match](#tag-match).                                                                                         |
| `entityIDs`       | uuid\[]   | Restrict to specific entity IDs.                                                                                                             |
| `importedFromApp` | uuid\[]   | Restrict to entities imported from one or more app IDs.                                                                                      |
| `allOf`           | object\[] | Nested filters — all must match (logical AND).                                                                                               |
| `anyOf`           | object\[] | Nested filters — at least one must match (logical OR).                                                                                       |
| `not`             | object    | A nested filter — exclude entities that match it (logical NOT).                                                                              |

### Name match

```json theme={null}
{
  "entityName": {
    "stringMatchType": "CONTAINS",
    "string": "prod"
  }
}
```

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

### Tag match

```json theme={null}
{
  "entityTag": {
    "key": "env",
    "value": "prod",
    "connectionId": "9b1c33a4-..."
  }
}
```

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

### Logical composition

`allOf`, `anyOf`, and `not` each take the same shape as the outer filter and can be nested to any depth. Use them to combine constraints.

```json theme={null}
{
  "entityTypes": ["RESOURCE"],
  "allOf": [
    { "entityTag": { "key": "env", "value": "prod" } },
    { "entityTag": { "key": "team", "value": "platform" } }
  ],
  "not": {
    "entityItemTypes": ["AWS_IAM_ROLE"]
  }
}
```

This reads as: resources tagged `env=prod` **and** `team=platform`, **excluding** AWS IAM roles.

## Access filters

`accessFilters` traverses the access graph. Use it to constrain results by *who* can access them, or *what* they can access. Both fields are `AccessEntityFilters` objects with the same shape as `nodeFilters`.

| Field            | Direction     | Question it answers                                                   |
| ---------------- | ------------- | --------------------------------------------------------------------- |
| `isAccessibleBy` | Inbound edge  | The returned entity is accessible by ≥ 1 entity matching this filter. |
| `hasAccessTo`    | Outbound edge | The returned entity has access to ≥ 1 entity matching this filter.    |

If both are provided, **both** must be satisfied. Direct and indirect (through groups, nested groups, etc.) access edges are considered.

`hasAccessTo` additionally accepts two fields that constrain the role on the access edge itself:

| Field           | Type      | Description                                            |
| --------------- | --------- | ------------------------------------------------------ |
| `roleNames`     | string\[] | Match the role name on the access edge (e.g. `write`). |
| `roleRemoteIds` | string\[] | Match the role remote ID on the access edge.           |

These fields are only honored inside `hasAccessTo` — they have no effect in `nodeFilters` or `isAccessibleBy`.

```json theme={null}
{
  "nodeFilters": { "entityTypes": ["RESOURCE"] },
  "accessFilters": {
    "isAccessibleBy": {
      "entityTypes": ["USER"],
      "entityTag": { "key": "contractor" }
    }
  }
}
```

This finds resources accessible by any user tagged `contractor` (any value).

## Response shape

```json theme={null}
{
  "type": "NODE",
  "edges": [
    {
      "node": {
        "id": "8b0f6e0c-4b71-4f3b-9c61-1d1d5e6a51e4",
        "name": "prod-readonly",
        "entityType": "RESOURCE",
        "entityItemType": "AWS_IAM_ROLE"
      },
      "cursor": "b3BhcXVlLWN1cnNvci0x"
    }
  ],
  "pageInfo": {
    "hasNextPage": true,
    "endCursor": "b3BhcXVlLWN1cnNvci0x",
    "hasPreviousPage": false,
    "startCursor": "b3BhcXVlLWN1cnNvci0w"
  }
}
```

Each edge is one matched entity. The `node.id` is the entity's Opal UUID — use it with other API endpoints (e.g. `GET /resources/{resource_id}`) to fetch full details.

## Pagination

Set `first` to control the page size (default `200`). When `pageInfo.hasNextPage` is `true`, pass `pageInfo.endCursor` as `after` on the next request to fetch the following page.

```json theme={null}
{
  "type": "NODE",
  "query": { "nodeFilters": { "entityTypes": ["USER"] } },
  "first": 500,
  "after": "b3BhcXVlLWN1cnNvci0x"
}
```

## Examples

### 1. List all users

```json theme={null}
{
  "type": "NODE",
  "query": {
    "nodeFilters": { "entityTypes": ["USER"] }
  }
}
```

### 2. AWS IAM roles tagged `env=prod`

```json theme={null}
{
  "type": "NODE",
  "query": {
    "nodeFilters": {
      "entityItemTypes": ["AWS_IAM_ROLE"],
      "entityTag": { "key": "env", "value": "prod" }
    }
  }
}
```

### 3. Groups whose name starts with "engineering"

```json theme={null}
{
  "type": "NODE",
  "query": {
    "nodeFilters": {
      "entityTypes": ["GROUP"],
      "entityName": { "stringMatchType": "STARTS_WITH", "string": "engineering" }
    }
  }
}
```

### 4. Resources accessible by a specific user

Use `isAccessibleBy` with that user's `entityIDs`.

```json theme={null}
{
  "type": "NODE",
  "query": {
    "nodeFilters": { "entityTypes": ["RESOURCE"] },
    "accessFilters": {
      "isAccessibleBy": {
        "entityIDs": ["29827fb8-f2dd-4e80-9576-28e31e9934ac"]
      }
    }
  }
}
```

### 5. Users with access to any AWS IAM role

```json theme={null}
{
  "type": "NODE",
  "query": {
    "nodeFilters": { "entityTypes": ["USER"] },
    "accessFilters": {
      "hasAccessTo": {
        "entityItemTypes": ["AWS_IAM_ROLE"]
      }
    }
  }
}
```

### 6. Users with `write` access to a specific GitHub repo

Combine `entityName`, `entityItemTypes`, and `roleRemoteIds` inside `hasAccessTo`.

```json theme={null}
{
  "type": "NODE",
  "query": {
    "nodeFilters": { "entityTypes": ["USER"] },
    "accessFilters": {
      "hasAccessTo": {
        "entityItemTypes": ["GIT_HUB_REPO"],
        "entityName": { "stringMatchType": "EQUALS", "string": "payments-service" },
        "roleRemoteIds": ["write"]
      }
    }
  }
}
```

### 7. Production resources excluding IAM roles, accessible by contractors

Combines `allOf`, `not`, and `accessFilters`.

```json theme={null}
{
  "type": "NODE",
  "query": {
    "nodeFilters": {
      "entityTypes": ["RESOURCE"],
      "allOf": [
        { "entityTag": { "key": "env", "value": "prod" } },
        { "entityTag": { "key": "team", "value": "platform" } }
      ],
      "not": { "entityItemTypes": ["AWS_IAM_ROLE"] }
    },
    "accessFilters": {
      "isAccessibleBy": {
        "entityTypes": ["USER"],
        "entityTag": { "key": "contractor" }
      }
    }
  },
  "first": 50
}
```

### 8. Full request with curl

```bash theme={null}
curl -X POST https://api.opal.dev/v1/queries/run \
  -H "Authorization: Bearer $OPAL_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "NODE",
    "query": {
      "nodeFilters": {
        "entityTypes": ["RESOURCE"],
        "entityTag": { "key": "env", "value": "prod" }
      },
      "accessFilters": {
        "isAccessibleBy": {
          "entityTypes": ["USER"],
          "entityTag": { "key": "contractor" }
        }
      }
    },
    "first": 100
  }'
```

## Limitations

The API has the same constraints as the OpalQuery UI. See [Limitations](/docs/opal-query#limitations) in the Overview.
