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

GitLab Orbit query language

  • Tier: Premium, Ultimate
  • Offering: GitLab.com
  • Status: Beta

The availability of this feature is controlled by a feature flag. For more information, see the history. This feature is available for testing, but not ready for production use.

Use the GitLab Orbit query language when you need GitLab data as a graph instead of a flat API response. A query is a JSON object. It names the entities to match, the relationships to follow, and the properties to return.

Request envelope

When submitting a query via the REST API or glab orbit remote query, wrap the query object in a top-level query field:

{
  "query": {
    "query_type": "traversal",
    "nodes": [{
      "id": "mr",
      "entity": "MergeRequest",
      "node_ids": [12345],
      "columns": ["iid", "title", "state"]
    }],
    "limit": 1
  },
  "response_format": "raw"
}
FieldRequiredDescription
queryYesThe query object documented below.
response_formatNo"llm" (default when omitted; compact GOON text optimized for LLM consumption) or "raw" (structured JSON). Use "raw" when piping output into jq.

The orbit query CLI (for local graphs) takes the raw query body without the envelope.

Query shape

Every query has a query_type and a nodes array of node selectors.

{
  "query_type": "traversal",
  "nodes": [{
    "id": "mr",
    "entity": "MergeRequest",
    "node_ids": [12345],
    "columns": ["iid", "title", "state"]
  }],
  "limit": 1
}

Query types

Query typeUse it to
traversalFetch matching nodes or follow relationships between nodes.
aggregationCount, sum, average, group, or sort matching graph results.
path_findingFind a bounded path between two node selectors.
neighborsReturn nodes connected to one bounded node.

Single-node traversal is the search shape. There is no separate search query type.

Top-level fields

FieldTypeDescription
query_typestringOne of traversal, aggregation, path_finding, or neighbors.
nodesarrayNode selectors. Always required; single-node queries (neighbors, search-shape traversal) use a 1-element array. Maximum 5.
relationshipsarrayRelationship selectors for traversal or aggregation. Maximum 5.
aggregationsarrayAggregation definitions. Required for aggregation. Maximum 10.
group_byarrayGroup keys for aggregation rows. Maximum 4.
pathobjectPath finding configuration. Required for path_finding.
neighborsobjectNeighbor lookup configuration. Required for neighbors.
limitintegerMaximum rows to return when no cursor is set. Default 30. Maximum 1000. Check pagination.truncated in the response: when true, more matching rows exist.
cursorobjectKeyset pagination: {"page_size": N} for the first page, then {"page_size": N, "after": "<pagination.next_cursor>"} until next_cursor is absent. Reaches every row regardless of dataset size. The token is bound to the exact query that issued it.
order_bystringSort rows by a node property: "node.property" (asc) or "-node.property" (desc).
aggregation_sortstringSort aggregation rows by output column (aggregation or group-key alias): "column" (asc) or "-column" (desc).
optionsobjectPresentation and debug options.

Pagination reads live data at request time; there is no snapshot. Each page independently resolves the latest version of every row and filters out soft-deleted rows, so version churn and tombstone cleanup between pages do not skip or duplicate results. Rows inserted after the cursor position in sort order appear on later pages; rows inserted or reordered behind it are not revisited. Rows whose sort key is NULL sort last and paginate like any other row. A row whose sort key changes between pages can appear twice or not at all, the same as any keyset pagination without a snapshot.

Node selectors

A node selector names one entity type in the ontology.

FieldTypeDescription
idstringLocal alias for the node. Relationships, aggregations, path, and neighbors refer to this alias.
entitystringOntology node type, such as Project, User, MergeRequest, File, or Definition.
columnsstring or arrayProperties to return. Use "*" for all non-restricted properties or an array of names. If omitted, GitLab Orbit returns the entity’s default columns.
filtersobjectProperty filters.
node_idsarrayExact IDs to match. Accepts integers or digit strings. Maximum 500.
id_rangeobjectInclusive ID range with start and end.
id_propertystringProperty used by node_ids and id_range. Default id.

Use node_ids when you already know the graph ID. Use filters when you know a natural property such as username, full_path, state, or path.

Relationships

Relationships connect node selectors by alias.

{
  "type": "AUTHORED",
  "from": "user",
  "to": "mr",
  "direction": "outgoing"
}
FieldTypeDescription
typestring or arrayRelationship type or types. Use "*" only when you need any relationship and have a bounded query.
fromstringAlias of the start node selector.
tostringAlias of the end node selector.
directionstringoutgoing, incoming, or both. Default outgoing.
hopsarrayInclusive [min, max] hop range ([1, 3]; [2, 2] for exactly 2). Default [1, 1]. Maximum 3.
filtersobjectRelationship property filters. Maximum 5 filters.

For example, merge requests point to projects with IN_PROJECT, and users point to merge requests with AUTHORED.

Filters

Filters can use simple equality:

{
  "filters": {
    "state": "merged"
  }
}

Or they can use an operator object. Multiple operator keys on the same property are AND-combined, which is how you express ranges:

{
  "filters": {
    "created_at": {"gte": "2026-01-01", "lt": "2026-02-01"},
    "state": {"in": ["opened", "merged"]}
  }
}

To repeat an operator on the same property, use an array of operator objects: {"title": [{"contains": "foo"}, {"contains": "bar"}]}.

OperatorUse
eqEqual to a scalar value.
gt, gte, lt, lteNumeric, date, or timestamp comparison.
inValue is in an array. Maximum 100 values.
containsString contains a substring.
starts_withString starts with a prefix.
ends_withString ends with a suffix.
is_nullNull check. Takes a boolean: false matches non-null.
is_not_nullNot-null check. Takes a boolean: false matches null.
token_matchText index contains one token.
all_tokensText index contains all tokens.
any_tokensText index contains any token.

Token operators work only on properties with text indexes.

Text-indexed properties

The following properties support token_match, all_tokens, and any_tokens. Using these operators on other properties falls back to a full string scan, which is slower.

EntityText-indexed properties
Branchname
Definitionfile_path, fqn, name
Deploymentref
Directoryname, path
Environmentenvironment_type, name
Filename, path
Findingdescription, name
Groupdescription, name
ImportedSymbolfile_path, import_path
Jobname, ref
Labeldescription, title
MergeRequestdescription, source_branch, target_branch, title
MergeRequestDiffFilenew_path, old_path
Milestonedescription, title
Notenote
Pipelineref
Projectdescription, name
Runnername
Stagename
Username, username
Vulnerabilitydescription, title
VulnerabilityIdentifierexternal_id, external_type, name
VulnerabilityOccurrencedescription, name
VulnerabilityScannerexternal_id, name
WorkItemdescription, title

Columns and virtual columns

Most columns come from indexed graph tables in ClickHouse. Some columns are virtual: GitLab Orbit fetches them from another service after the graph query returns.

Request virtual columns explicitly in columns. The dynamic_columns option used by path_finding and neighbors excludes virtual columns because they can require external service calls.

EntityVirtual columnWhat it returns
MergeRequestdiffFull unified diff for the merge request.
MergeRequestDiffpatchFull patch for one merge request diff snapshot.
MergeRequestDiffFilediffPer-file unified diff text. Returns null when too_large is true.
FilecontentRaw source text of a file.
DefinitioncontentSource text for one indexed definition.

The content column is for source code. For merge request diff text, use MergeRequest.diff, MergeRequestDiff.patch, or MergeRequestDiffFile.diff.

Traversal examples

Fetch one merge request with its full diff:

{
  "query_type": "traversal",
  "nodes": [{
    "id": "mr",
    "entity": "MergeRequest",
    "node_ids": [12345],
    "columns": ["iid", "title", "state", "diff"]
  }],
  "limit": 1
}

Fetch per-file diff content from diff snapshots:

{
  "query_type": "traversal",
  "nodes": [
    {
      "id": "mr",
      "entity": "MergeRequest",
      "node_ids": [12345],
      "columns": ["iid", "title", "state"]
    },
    {
      "id": "snapshot",
      "entity": "MergeRequestDiff",
      "columns": ["id", "state", "patch"]
    },
    {
      "id": "file",
      "entity": "MergeRequestDiffFile",
      "columns": ["new_path", "old_path", "too_large", "diff"]
    }
  ],
  "relationships": [
    {"type": "HAS_DIFF", "from": "mr", "to": "snapshot"},
    {"type": "HAS_FILE", "from": "snapshot", "to": "file"}
  ],
  "limit": 20
}

HAS_DIFF returns every diff snapshot the merge request ever had (MergeRequestDiff.merge_request_id FK). HAS_LATEST_DIFF returns only the most recent snapshot (MergeRequest.latest_merge_request_diff_id FK) — useful for “what does the merge request look like right now”, but not for historical questions. For “every merge request that ever touched a file”, traverse HAS_DIFF over all snapshots. Using HAS_LATEST_DIFF for historical-coverage questions can substantially undercount on long-lived files: an MR that touched the file in an earlier revision but not in its final diff is invisible through HAS_LATEST_DIFF.

MergeRequestDiffFile.old_path is the preferred column for file lookups; new_path differs from old_path only on renames. Filtering and grouping by old_path keeps the same row identity across an MR’s history. See the ontology field descriptions on merge_request_diff_file.yaml.

Fetch source file content:

{
  "query_type": "traversal",
  "nodes": [{
    "id": "file",
    "entity": "File",
    "filters": {
      "path": {"ends_with": "app/models/project.rb"}
    },
    "columns": ["path", "language", "content"]
  }],
  "limit": 5
}

Fetch the source text of a specific function or class definition. The content column returns the raw source text of just that definition, not the full file. Use fqn (fully-qualified name) for an exact match, or name with contains for a broader search:

{
  "query_type": "traversal",
  "nodes": [{
    "id": "d",
    "entity": "Definition",
    "filters": {
      "fqn": {"eq": "Gitlab::Auth::authenticate"}
    },
    "columns": ["name", "fqn", "file_path", "start_line", "end_line", "content"]
  }],
  "limit": 5
}

Find merged merge requests in a project:

{
  "query_type": "traversal",
  "nodes": [
    {
      "id": "project",
      "entity": "Project",
      "filters": {"full_path": "your-group/your-project"},
      "columns": ["name", "full_path"]
    },
    {
      "id": "mr",
      "entity": "MergeRequest",
      "filters": {"state": "merged"},
      "columns": ["iid", "title", "state", "merged_at"]
    }
  ],
  "relationships": [
    {"type": "IN_PROJECT", "from": "mr", "to": "project"}
  ],
  "limit": 25
}

Find every pipeline that ran for one merge request. Always filter Pipeline.source = "merge_request_event" to match what the merge request’s Pipelines tab shows:

{
  "query_type": "traversal",
  "nodes": [{
    "id": "p",
    "entity": "Pipeline",
    "filters": {
      "merge_request_id": {"eq": 482908721},
      "source": {"eq": "merge_request_event"}
    },
    "columns": ["id", "status", "source", "sha", "ref", "created_at"]
  }],
  "order_by": "-p.created_at",
  "limit": 100
}

merge_request_id is the merge request’s internal numeric id, not the project-scoped iid. Look it up first with a MergeRequest traversal that filters by iid and project_id, then plug the id into the query above.

Both Pipeline.merge_request_id and the MergeRequest --TRIGGERED--> Pipeline edge link an MR to every CI pipeline spawned in its context, including the downstream child pipelines (source = "parent_pipeline") that the top-level MR pipelines trigger. Without the source = "merge_request_event" filter, the result over-counts by a large factor on any MR that uses parent-child pipeline fan-out, and does not match what the MR Pipelines tab shows. Apply the same filter when traversing MergeRequest --TRIGGERED--> Pipeline in a multi-node query.

MergeRequest --HAS_HEAD_PIPELINE--> Pipeline is a different edge. It points to the single most recent pipeline running against the tip of the merge request’s source branch. Use it for “what is currently running”, not for pipeline history.

Aggregation

Aggregation queries use aggregations. Each aggregation is an object with a single function key whose value is what to aggregate, plus an optional as output column name: {"avg": "mr.merge_duration", "as": "avg_dur"}.

Function keyValueSupported property types
count"node" (count matching rows) or "node.property" (count non-null values)Any
sum"node.property"Numeric only
avg"node.property"Numeric only
min"node.property"Numeric, string, boolean, Date, or DateTime
max"node.property"Numeric, string, boolean, Date, or DateTime

Without as, the output column name is derived as <function>_<node> (count_mr) or <function>_<node>_<property> (avg_mr_merge_duration). Reference these names in aggregation_sort.

sum and avg reject DateTime properties with a validation error. To aggregate over dates, use min or max.

Use top-level group_by to group aggregation rows. It applies to every aggregation in the query. Do not put grouping inside an individual aggregation.

Group keys support these shapes:

Group keyShapeResult value
Node"<node-id>" (e.g. "p")A nested entity object in each row.
Property"<node-id>.<property>" (e.g. "mr.state")A scalar bucket value in each row.
Truncated date{"key": "<node-id>.<property>", "truncate": "<unit>"}The property truncated to the start of the unit.

Output column names are derived: node keys use the node ID (p), property keys use <node>_<property> (mr_state), and truncated keys append the unit (mr_created_at_month). Reference these names in aggregation_sort. Duplicate group or aggregate output names are rejected.

Use the derived names. Only when a consumer requires a specific column name, rename with the object form’s optional as: {"key": "mr.state", "as": "state"}, or with truncation {"key": "mr.created_at", "truncate": "month", "as": "month"}.

Truncation units are minute, hour, day, week, month, quarter, and year, and apply only to Date/DateTime properties. minute and hour require node_ids or a filter on the truncated property to bound bucket cardinality.

Property groups must reference a real ClickHouse-backed, filterable property that the caller is allowed to use. Virtual fields and unfilterable fields are rejected during validation.

Count merged merge requests per project:

{
  "query_type": "aggregation",
  "nodes": [
    {
      "id": "project",
      "entity": "Project",
      "filters": {"full_path": "your-group/your-project"}
    },
    {
      "id": "mr",
      "entity": "MergeRequest",
      "filters": {"state": "merged"}
    }
  ],
  "relationships": [
    {"type": "IN_PROJECT", "from": "mr", "to": "project"}
  ],
  "group_by": ["project"],
  "aggregations": [
    { "count": "mr", "as": "merged_mrs" }
  ],
  "aggregation_sort": "-merged_mrs",
  "limit": 10
}

Count detected vulnerabilities by severity:

{
  "query_type": "aggregation",
  "nodes": [
    {
      "id": "v",
      "entity": "Vulnerability",
      "filters": {"state": "detected"}
    }
  ],
  "group_by": ["v.severity"],
  "aggregations": [
    { "count": "v", "as": "vulnerability_count" }
  ],
  "aggregation_sort": "-vulnerability_count",
  "limit": 10
}

Aggregation responses are table-shaped. columns describes computed aggregate values, group_columns describes grouping keys, and rows carries group values plus metric values. Node-grouped rows store the grouped entity under the group key. Property-grouped rows store the scalar bucket under the group key.

collect is listed in the input type but currently rejected by validation.

Path finding

Path finding queries use path.

FieldTypeDescription
typestringshortest.
fromstringAlias of the start node selector.
tostringAlias of the end node selector.
max_depthintegerMaximum path length. Maximum 3.
rel_typesarrayRelationship types to traverse. Required unless both endpoints use node_ids.

Both endpoints must be bounded by node_ids, filters, or an id_range with a span of 500 or less. If either endpoint uses filters or id_range, provide rel_types.

{
  "query_type": "path_finding",
  "nodes": [
    {"id": "start", "entity": "Project", "node_ids": [278964]},
    {"id": "end", "entity": "User", "node_ids": [1]}
  ],
  "path": {
    "type": "shortest",
    "from": "start",
    "to": "end",
    "max_depth": 3,
    "rel_types": ["CREATOR", "AUTHORED", "IN_PROJECT"]
  },
  "limit": 5
}

Neighbors

Neighbor queries use a 1-element nodes array and a neighbors object. The center node must be bounded by node_ids, filters, or a narrow id_range.

{
  "query_type": "neighbors",
  "nodes": [{
    "id": "mr",
    "entity": "MergeRequest",
    "node_ids": [12345]
  }],
  "neighbors": {
    "direction": "both",
    "rel_types": ["AUTHORED", "IN_PROJECT", "HAS_DIFF"]
  },
  "options": {
    "dynamic_columns": "default"
  },
  "limit": 25
}

Set options.dynamic_columns to "*" if you need all non-restricted ClickHouse-backed columns for dynamically discovered neighbor or path nodes. Virtual columns still require an explicit request in a traversal query.

Validation limits

GitLab Orbit rejects broad or ambiguous queries before compiling SQL.

LimitValue
Nodes per query5
Relationships per query5
Aggregations per query10
node_ids per selector500
Values in an in filter100
Columns per node selector50
Relationship types per selector10
Relationship hops3
Path depth3
Filters per node10
Filters per relationship5

Traversal and aggregation queries must include at least one selective node: node_ids, filters, or an id_range with a span of 100,000 or less.

Single-node traversal also requires selectivity. To inspect a broad entity, add a filter, provide IDs, or use a narrow id_range.

Options

OptionDescription
dynamic_columnsFor path_finding and neighbors hydration. Use default for each entity’s default columns, or "*" for all non-restricted ClickHouse-backed columns. Default default.
include_debug_sqlInclude compiled ClickHouse SQL in response metadata when the caller is allowed to see it.