GitLab Orbit query language
- Tier: Premium, Ultimate
- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated
GitLab Orbit accepts GQL, a read-only, OpenCypher-like query language that agents already know.
To send a query, see run a query.
The examples use the gitlab-org/gitlab project on GitLab.com.
To run them on your own projects, see bounded nodes.
Supported syntax
GQL supports:
MATCHwith one connected pattern, and more than oneMATCHclause in a queryWHEREwithAND,NOT, comparisons,IN,STARTS WITH,ENDS WITH,CONTAINS,IS NULL, andIS NOT NULLRETURNwith properties, nodes,*,properties(n),n{.property}, andASaliases- The aggregate functions
count,count(DISTINCT x),sum,avg,min, andmax, anddate_truncfor time periods ORDER BYwith one sort key,SKIP(orOFFSET), andLIMIT- Variable-length relationships up to three hops, and shortest paths with
ANY SHORTESTorSHORTEST 1 type(r), and the text search functionstoken_match,all_tokens, andany_tokensCALL db.schema()andDEBUG
GQL does not support these forms, and rejects them:
- Write clauses, such as
CREATE,MERGE,SET, andDELETE, and more than one statement OPTIONAL MATCH,WITH,UNION,UNWIND,YIELD, and subqueries- Patterns that are not connected or that have a cycle
OR,XOR, andx NOT IN [...](writeNOT x IN [...]instead)DISTINCToutsidecount(), such asRETURN DISTINCTorsum(DISTINCT x)count(*)and arbitrary expressions- More than one
ORDER BYkey - Query parameters
- Other functions, such as
collect(x)(the error names the function)
Discover the schema
To list the properties and relationships of one node type, run:
glab orbit query "CALL db.schema('MergeRequest')"CALL db.schema() lists every node type and relationship type.
Node names are case-sensitive. Issues, epics, tasks, and incidents are all the WorkItem node.
For the full list, see indexed data.
Statement shape
MATCH pattern [WHERE predicates]
[MATCH pattern [WHERE predicates] ...]
RETURN projections
[ORDER BY key [ASC | DESC]]
[SKIP rows]
[LIMIT rows]
[DEBUG]- Each node needs a variable. Declare the label of each node, and any inline properties, where the node first appears:
(mr:MergeRequest {iid: 235291}). Later parts of the query can reuse the bare variable. Only the far node of a neighbors query has no label. - Write a relationship type after a colon:
-[:IN_PROJECT]->. A lowercase name without a colon, such as-[r]->, is a variable that matches any relationship type. GitLab Orbit rejects an uppercase name without a colon, such as-[IN_PROJECT]->. - To match one of several relationship types, separate them with
|, for example-[:AUTHORED|ASSIGNED]->. Relationship type names are case-sensitive. - Use
->or<-between labeled nodes. Use undirected--only to find the neighbors of a node. - All parts of the pattern must form one connected tree, with no cycles. Each later part of the pattern must start from a node that an earlier part declares.
Bounded nodes
Every query needs at least one node bounded by an ID or a literal property filter,
such as {id: 278964}, {full_path: 'gitlab-org/gitlab'}, or a WHERE comparison with a literal value.
LIMIT does not count, so GitLab Orbit rejects MATCH (p:Project) RETURN p LIMIT 5.
A neighbors query needs a bounded center node. A shortest path query needs both endpoints bounded.
The examples use numeric IDs, such as project_id: 278964.
An id is unique in the instance.
An iid, such as the number of a merge request, is unique only in its project.
To find an ID, query by path first:
MATCH (mr:MergeRequest {iid: 235291})-[:IN_PROJECT]->(p:Project {full_path: 'gitlab-org/gitlab'})
RETURN mr.id, p.id
LIMIT 1On GitLab.com, this query returns 482908721 and 278964, the IDs that the examples use.
It returns them only if you have the Reporter, Security Manager, Developer, Maintainer, or Owner role for the gitlab-org group.
Otherwise, this query and the examples that use gitlab-org/gitlab or these IDs return no rows.
To run the examples, query with the full path of one of your projects, and use the IDs that it returns.
Find the neighbors of a node
To find every node that is connected to one node, write an undirected relationship, --, from a bounded node:
MATCH (mr:MergeRequest {id: 482908721})--(n)
RETURN n
LIMIT 20The far node, n, takes no filters.
Find the shortest path between two nodes
To find the shortest path between two bounded nodes, use ANY SHORTEST.
Name the relationship type, such as -[:CONTAINS*1..2]->, and keep the hop range small.
A shortest path with no relationship type, such as -[*1..3]->, fails.
The path uses ->, starts at one hop, and is the only MATCH clause. RETURN takes only the path variable.
If the filter on an endpoint matches more than 500 nodes, the search starts from the 500 nodes with the lowest IDs.
MATCH path = ANY SHORTEST (g:Group {id: 9970})-[:CONTAINS*1..2]->(p:Project {full_path: 'gitlab-org/gitlab'})
RETURN pathFilters
WHERE combines predicates with AND and NOT.
To exclude values, write NOT x.state IN [...], not x.state NOT IN [...].
These forms are supported:
- Comparisons:
=,<>or!=,<,<=,>,>=. You can compare a property with a literal. In a query without aggregation, you can also compare two node properties of the same type. A comparison between two properties does not bound a node. - Lists:
x.state IN ['opened', 'merged'], with up to 100 values, or up to 500 values inx.id IN [...]. - Strings:
STARTS WITH,ENDS WITH, andCONTAINS. The pattern needs at least three characters. Matching ignores ASCII case, except on thebranchproperty of code nodes, such asFileandDefinition.=andINare case-sensitive. - Nulls:
IS NULLandIS NOT NULL.
Values are literals.
A string is in single or double quotes.
Write a control character as an escape: \n, \t, \r, \b, \f, \uXXXX, or \UXXXXXXXX. Also escape \\, \', and \".
A string that contains a raw control character, such as a newline or a tab, fails.
For a timestamp property, such as mr.created_at, use a string in the format YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS,
with optional fractional seconds and a trailing Z, such as '2026-01-31' or '2026-01-31T12:00:00Z'.
A space can replace the T.
A date property, such as WorkItem.due_date, takes only YYYY-MM-DD.
A value with a time zone offset, such as +02:00, fails.
On a date or timestamp property, you can wrap the string in date() or datetime(), and the result is the same.
On other properties, the wrapper fails.
When you compare a date property with a timestamp property, the date counts as midnight.
Some timestamp properties, such as mr.merged_at, reject dates on or before 1970-01-01.
To find rows without a value, use IS NULL.
An enum property, such as mr.state, accepts only its declared values with =, <>, and IN.
Enum values have no order, so <, <=, >, and >= fail.
An unknown value fails, and the error lists the valid values.
On a relationship, source_kind and target_kind must name the label of the matching endpoint node,
and relationship_kind must name a type in the pattern.
For example, (mr:MergeRequest)-[r:IN_PROJECT]->(p:Project) rejects r.target_kind = 'Group'.
Group predicates with parentheses. Queries do not take parameters, so do not insert untrusted text into a query.
Text search
CONTAINS is indexed on text-indexed properties, such as MergeRequest.title or Definition.fqn.
Use it for a word prefix, such as 'migrat', or for a phrase with its words in order, such as 'fix flaky'.
When word order does not matter, use the token functions.
Words are runs of letters and digits, so token_match(d.file_path, 'main') matches src/main.rs.
token_match(property, 'token')matches one whole word.all_tokens(property, 'first second')requires every word, in any order.any_tokens(property, 'first second')requires at least one of the words.
Properties without a text index reject the token functions. The value needs at least three characters, and matching ignores ASCII case. This query finds merge requests with both words in the title, in any order:
MATCH (mr:MergeRequest {project_id: 278964})
WHERE all_tokens(mr.title, 'database migration')
RETURN mr.iid, mr.title, mr.state
ORDER BY mr.created_at DESC
LIMIT 10These properties have a text index:
| Entity | Text-indexed properties |
|---|---|
Branch | name |
ContainerRepository | name |
Definition | file_path, fqn, name |
Dependency | name |
Deployment | ref |
Directory | name, path |
Environment | environment_type, name |
File | name, path |
Finding | description, name |
Group | description, name |
ImportedSymbol | file_path, import_path |
Job | name, ref |
Label | description, title |
MergeRequest | description, source_branch, target_branch, title |
MergeRequestDiff | head_commit_sha |
MergeRequestDiffFile | new_path, old_path |
Milestone | description, title |
Note | note |
Package | name |
PackageFile | file_name |
Pipeline | ref |
Project | description, name |
Runner | name |
Stage | name |
User | name, username |
Vulnerability | description, title |
VulnerabilityIdentifier | external_id, external_type, name |
VulnerabilityOccurrence | description, name |
VulnerabilityScanner | external_id, name |
WorkItem | description, title |
Return values
RETURN mr.iid, mr.titlereturns properties.RETURN mrreturns the default properties of the node, for exampleid,iid,title,state,source_branch, andtarget_branchforMergeRequest.properties(mr)returns all properties of the node.mr{.iid, .title}returns the listed properties.RETURN mrandproperties(mr)do not include properties for administrators, such asUser.is_admin. To get one, name it, as inRETURN u.is_admin. For users who are not administrators, a query that names one fails.- The response always includes the identity of each node and the metadata of each relationship.
type(r)returns the type of a one-hop relationship variable inWHERE,RETURN, andORDER BY, but not as an aggregation group key.ORDER BYsorts by a node property, a property alias, ortype(r). A relationship property fails. In an aggregation,ORDER BYsorts by an aggregate alias or a returned group key. Neighbors and shortest path queries do not acceptORDER BY.
Aggregate results
- The aggregate functions are
count,sum,avg,min, andmax. The other items inRETURNbecome group keys. For users who are not administrators, an aggregation needs a node that belongs to a group or project, such asProjectorMergeRequest. Neighbors and shortest path queries cannot aggregate. To sort by an aggregate, give it an alias:RETURN mr.state, count(mr) AS mrs ORDER BY mrs DESC.sumandavgneed a numeric property.minandmaxneed a number, boolean, date, or timestamp property.DISTINCTworks only incount(), as incount(DISTINCT mr.author_id). In an aggregation, a text match or a<,<=,>, or>=comparison on a free-text property, such asmr.title, needs that node in a group key: return the node, as inRETURN mr, count(p), or one of its free-text properties, as inRETURN mr.title, count(p). Other properties, such asmr.iid, do not count. Otherwise, the query fails withtext matching and ordering require grouping by "mr".Project,Group,AgentSession,Branch,Commit, and the code nodesDefinition,Directory,File, andImportedSymbolare exempt. - To group by a time period, use
date_trunc('<unit>', <property>)as a group key on a date or timestamp property. The units areminute,hour,day,week,month,quarter, andyear.minuteandhouralso need a filter on the same property, or anid =orid INfilter on the same node. Weeks start on Sunday.
MATCH (mr:MergeRequest)-[:IN_PROJECT]->(p:Project {full_path: 'gitlab-org/gitlab'})
WHERE mr.created_at >= '2026-01-01T00:00:00Z'
RETURN date_trunc('month', mr.created_at) AS month, count(mr) AS mrs
ORDER BY month
LIMIT 12Pagination
To get results page by page, use SKIP and LIMIT, and add the page size to SKIP for each next page:
SKIP 0 LIMIT 20, then SKIP 20 LIMIT 20, then SKIP 40 LIMIT 20.
Write SKIP on every page, including SKIP 0 on the first page, so that all pages use the same sort order.
Do not change other parts of the query between pages.
MATCH (mr:MergeRequest)-[:IN_PROJECT]->(p:Project {full_path: 'gitlab-org/gitlab'})
WHERE mr.state = 'merged'
RETURN mr.iid, mr.title
ORDER BY mr.created_at DESC
SKIP 20
LIMIT 20In the raw response format (--response-format raw in the CLI), pagination.has_more is true while more rows exist.
Stop when pagination.has_more is false.
A page can return fewer rows than LIMIT, or no rows, because GitLab removes the rows that you cannot access.
SKIP also counts these rows, so pages do not overlap.
pagination.truncated has the same value as pagination.has_more.
SKIP accepts up to 10,000 rows.
To go deeper, sort by a unique property, such as id, and filter past the last value of the previous page:
MATCH (mr:MergeRequest)-[:IN_PROJECT]->(p:Project {full_path: 'gitlab-org/gitlab'})
WHERE mr.state = 'merged' AND mr.id > 482908721
RETURN mr.id, mr.iid, mr.title
ORDER BY mr.id
LIMIT 1000Use the last mr.id of each page as the lower bound for the next page.
Each page reads the current data. If a row is added or deleted before your position between two pages, the later pages move by one row, so a row can repeat or be missing.
Limits
- Each relationship spans at most three hops.
-[:CONTAINS*1..3]->is a bounded variable-length hop. - A query has at most five nodes. The pattern is a tree, so it has at most four relationships.
- A relationship names at most 10 types.
- A query has at most 16 conditions. Inline properties, such as
{project_id: 278964}, count as conditions. - Each node filters on at most 10 properties, and each relationship on at most 5. Each property takes at most 10 conditions.
- Parentheses, brackets, and braces nest at most 32 levels deep, and a condition has at most 32
NOTs. - An aggregation has at most 10 aggregate functions and 4 group keys.
ORDER BYtakes one sort key.LIMITaccepts 1 to 1000 rows. The default is 30.SKIPaccepts 0 to 10,000 rows.- Each node returns at most 50 properties and filters on at most 500 IDs.
- Names, such as variables, aliases, labels, and properties, have at most 64 characters.
- Strings have at most 1024 characters.
- The query text is at most 32 KiB.
Debug a query
Syntax errors report a line and column.
To see the SQL that GitLab Orbit runs, end the query with DEBUG and use the raw response format (--response-format raw).
On GitLab.com, the response includes the SQL only for GitLab team members.
On GitLab Self-Managed and GitLab Dedicated, the response includes the SQL only for administrators.
For other users, GitLab ignores DEBUG.