Docs · Guides

Probara Query Language (PQL) reference

Probara Query Language (PQL) asks questions of your test data. A query names an entity, such as cases or results, filters its rows with WHERE, and can group and aggregate them. Write PQL in Queries, or send it to the PQL API.

Quick path

  1. Name the entity: FROM cases.
  2. Filter the rows: WHERE priority = "High".
  3. Choose the page: LIMIT 50.
FROM cases WHERE priority = "High" AND status = "Active" ORDER BY title LIMIT 50

Query shape

ClausePurposeNotes
SELECTColumns and aggregatesOptional. Without it, a query returns the entity’s default columns.
FROMThe entityRequired. One of the nine entities in Entities.
WHEREConditions on each rowRuns before grouping.
GROUP BYGroupsFields, expressions or positions, such as GROUP BY 1.
HAVINGConditions on groupsUses aggregates, such as HAVING count() > 5.
ORDER BYSort orderFields, aliases, positions or aggregates. Add DESC for descending.
LIMIT and OFFSETPage size and startLIMIT defaults to 50 and takes up to 500. OFFSET follows LIMIT.

Rules:

  • Keywords, function names, entity names and field names are not case-sensitive.
  • A line comment starts with --.
  • Line breaks and extra spaces do not matter.
  • A query with SELECT and an aggregate returns grouped rows. Any other query returns rows.
  • A SELECT item that is not aggregated must appear in GROUP BY.

Literals

KindHow to write itExample
TextIn double or single quotes.title ~ "login"
NumberAn integer or a decimal.steps >= 3
Booleantrue or false.archived = false
ListValues in parentheses, separated by commas. At most 200 values.priority in ("High", "Critical")
Fixed durationA number and a unit: m minutes, h hours, d days, w weeks.duration > 30m
Relative durationA signed duration in a time function. Units also include mo months, q quarters and y years.now(-3mo), startOf(month, -1mo)
Date"YYYY-MM-DD" or "YYYY-MM-DD HH:mm", in quotes.created > "2026-10-01"
Enum or option valueA bare word or a quoted value. Matching ignores capital letters.status = failed, priority = "High"

Notes:

  • Text values must be quoted. title = Login is an error, and title = "Login" works.
  • Names of milestones, suites, environments, tags and projects are text. Quote them when they contain spaces: milestone = "Release 1.4".
  • Fixed-duration fields, such as runs.duration, accept m, h, d and w only. Use mo, q and y only in relative time.

Operators

OperatorWhat it doesField types
=, !=Equal, or not equalAll fields except lastResult
<, <=, >, >=Before, after, less than, greater thanNumber, duration, date
between a and bFrom a to bNumber, duration, date
~, !~Contains, or does not contain, ignoring capital lettersText
startsWithBegins with the textText
in (...), not in (...)One of a list, or none of itAll fields except boolean and lastResult
in query("Name")Matches the rows of a saved query. See In query() references.id only
is empty, is not emptyNo value, or a valueFields marked as optional in the entity tables
under "Suite"The suite and every suite below itSuite references: cases.suite, suites.parent
exists(...), count(...) <op> nRelated records. See Relations.Entities with relations
was, changedChange history. See History.Fields marked as history-tracked

Boolean fields accept only = and !=, so write archived = true, not archived.

Entities

Every entity has an id, which is its display ID, and a project. The tables list the fields you can use. A field marked Optional can be empty, so is empty applies to it. A field marked History supports was and changed.

Cases

Default columns: id, title, suite, priority, status.

FieldTypeMeaningHistory
idIDThe case’s display ID
projectReferenceThe project
titleTextThe title
descriptionText, optionalThe description
suiteReference, optionalThe suite. Use under for its tree.
milestoneReference, optionalThe milestone
tagsTags, optionalThe tags. A case counts in the group of each of its tags.
authorUserWho created the case
updatedByUser, optionalWho last updated the case
createdDateWhen the case was created
updatedDateWhen the case was last updated
archivedBooleanWhether the case is archived. See Archived cases.
automationKeyText, optionalThe automation key
automatedBooleanWhether the case has an automation key
stepsNumberThe number of steps
revisionNumberThe revision number
reviewStatusEnumnot_reviewed, in_review or approved. See Test case review.
jiraText, optionalThe first linked Jira issue key
priorityOption, optionalThe priority, from your organization’s optionsYes
severityOption, optionalThe severity, from your organization’s optionsYes
statusOption, optionalThe status, from your organization’s optionsYes
typeOption, optionalThe type, from your organization’s optionsYes
layerOption, optionalThe layer, from your organization’s optionsYes
behaviorOption, optionalThe behavior, from your organization’s optionsYes
isFlakyBooleanWhether the case is marked as flaky
lastResultResultThe most recent result. Use lastResult.field. See lastResult.

Relations: results, runCases, defects, jira. Custom fields: cf[...].

Suites

Default columns: id, name, parent, cases.

FieldTypeMeaning
idIDThe suite’s display ID
projectReferenceThe project
nameTextThe name
parentReference, optionalThe parent suite. Use under for its tree.
pathTextThe suite’s path
casesNumberThe number of cases that are not archived
createdDateWhen the suite was created
updatedDateWhen the suite was last updated

Relations: cases.

Runs

Default columns: id, name, state, status, milestone, passRate.

FieldTypeMeaning
idIDThe run’s display ID
projectReferenceThe project
nameTextThe name
stateEnumopen or closed
statusEnumin_progress, passed, failed or aborted. An open run is in progress, and an aborted run is aborted.
environmentReference, optionalThe environment
milestoneReference, optionalThe milestone
planReference, optionalThe test plan
authorUserWho created the run
tagsTags, optionalThe tags
started, finished, closed, abortedDate, optionalWhen the run started, finished, was closed, or was aborted
durationDuration, optionalThe total time of the run
passed, failed, blocked, skipped, untestedNumberThe number of cases with each outcome
passRateNumberThe share of executed cases that passed, as a percentage from 0 to 100, rounded to one decimal. It cannot be averaged or summed. Use passRate() in an aggregate query.
branchText, optionalThe branch
commitText, optionalThe commit

Relations: results, runCases, defects. Custom fields: cf[...].

Run cases

Run cases are the cases inside a run, including the ones not yet tested. Default columns: run, case, status, assignee, elapsed.

FieldTypeMeaning
idIDThe display ID
projectReferenceThe project
runReferenceThe run. Use run.field for its fields.
caseReferenceThe case. Use case.field for its fields.
statusEnumuntested, retested, passed, failed, skipped or blocked
assigneeUser, optionalWho the case is assigned to
elapsedDuration, optionalThe time spent on the case
parametersText, optionalThe instance’s combination as name=value pairs joined with ”, ”. ~ matches a pair or a value, for example FROM runCases WHERE parameters ~ "Browser=Firefox". A run case without parameters has none.

No relations.

A run case of a parameterized case is one instance: a case with two parameters of three values each has nine run cases in a run. Count them with count() over the run cases, and see Parameterized test cases.

Results

Default columns: id, run, case, status, executedBy, executed.

FieldTypeMeaning
idIDThe display ID, such as RES-42
projectReferenceThe project
caseReferenceThe case. Use case.field for its fields.
runReferenceThe run. Use run.field for its fields.
statusEnumpassed, failed, skipped or blocked
durationDuration, optionalHow long the execution took
executedByUser, optionalWho executed the case
executedDateWhen the case was executed
commentText, optionalThe comment

No relations. Custom fields: none.

Defects

Default columns: id, title, status, severity, priority, assignee.

FieldTypeMeaningHistory
idIDThe display ID
projectReferenceThe project
titleTextThe title
statusEnumopen, in_progress, resolved or closedYes
resolutionEnum, optionalfixed, wont_fix, duplicate or cannot_reproduce
severityOption, optionalThe severity, from your organization’s optionsYes
priorityOption, optionalThe priority, from your organization’s optionsYes
reporterUserWho reported the defect
assigneeUser, optionalWho the defect is assigned toYes
milestoneReference, optionalThe milestone
tagsTags, optionalThe tags
createdDateWhen the defect was created
updatedDateWhen the defect was last updated
resolvedDate, optionalWhen the defect was resolved
closedDate, optionalWhen the defect was closed
jiraText, optionalThe linked Jira issue key

Relations: cases, results. Custom fields: cf[...].

Milestones

Default columns: id, name, status, due.

FieldTypeMeaning
idIDThe display ID
projectReferenceThe project
nameTextThe name
statusEnumupcoming, active or completed
parentReference, optionalThe parent milestone
startDate, optionalThe start date
dueDate, optionalThe due date
completedDate, optionalWhen the milestone was completed

Relations: runs, cases, defects.

Plans

Default columns: id, name, status, milestone.

FieldTypeMeaning
idIDThe display ID
projectReferenceThe project
nameTextThe name
statusEnumdraft, active or completed
milestoneReference, optionalThe milestone
createdDateWhen the plan was created
updatedDateWhen the plan was last updated

Relations: runs, cases.

Shared steps

Default columns: id, title, folder, updated.

FieldTypeMeaning
idIDThe display ID
projectReference, optionalThe project. Empty means the shared step is organization-wide.
titleTextThe title
folderReference, optionalThe folder
createdDateWhen the shared step was created
updatedDateWhen the shared step was last updated

No relations.

Custom fields

Cases, runs and defects have your organization’s custom fields. Write the field’s title in brackets:

FROM cases WHERE cf["Risk"] = "High"

The title is matched ignoring capital letters and surrounding spaces. A title that no custom field of that entity has returns unknown_custom_field.

Custom field typeWorks asOperators
NumberNumberComparisons, between, lists
Short text, paragraph, URLText~, !~, startsWith, lists, =
CheckboxBoolean=, !=
Date pickerDateComparisons, between, lists
User pickerUser=, lists, and the member functions
Select, multi-select, radioOption=, lists. A multi-select counts a row in each of its values.

Results and run cases link to a run and a case. Reach into them with a dot path:

FROM results WHERE run.environment = "staging" AND case.priority = "Critical" AND status = failed

Paths work in WHERE and in GROUP BY, for example GROUP BY run.environment.

lastResult

lastResult is the most recent result of a case, by execution time and then by ID. It is available on cases at the top level of a query.

  • lastResult.status reads the status of the latest result.
  • lastResult(environment = "staging").status reads the latest result among the runs that match the condition. The condition uses run fields, such as environment, milestone and plan.
  • A case with no matching result has no value, so = does not match it.
FROM cases WHERE lastResult(environment = "staging").status = failed ORDER BY title

lastResult cannot be compared directly, and it has no history.

Relations

exists and count look at the related records of each row.

EntityRelations
casesresults, runCases, defects, jira
suitescases
runsresults, runCases, defects
milestonesruns, cases, defects
plansruns, cases
defectscases, results
OthersNone
FROM cases WHERE exists(results where status = failed)
FROM cases WHERE NOT exists(results)
FROM cases WHERE count(results where status = failed) >= 3

Rules:

  • Use exists and count in WHERE. exists in HAVING returns relation_placement.
  • Compare count(relation) with a number.
  • Sub-queries nest at most two levels deep. FROM suites WHERE exists(cases where exists(results where status = failed)) is two levels.
  • Related rows count only in projects you can access, and each related record counts once.
  • Archived cases are left out of a related-record condition over cases unless the condition names archived. See Archived cases.

Functions

FunctionReturnsNotes
currentUser()The signed-in userUse it on user fields and in by.
membersOf("Group")The members of a groupUse it inside in (...) on a user field.
activeMembers()The current members of the organizationUse it inside in (...) on a user field.
formerMembers()Former members of the organizationUse it inside in (...) on a user field.
now([offset])The current time, optionally shiftedThe offset is a signed duration, such as now(-7d).
startOf(unit[, offset])The start of the current hour, day, week, month, quarter or yearstartOf(week), startOf(month, -1mo)
endOf(unit[, offset])The end of the same unitsendOf(day)
count()The number of rows in each groupcount(*) also works.
count(distinct field)The number of distinct valuescount(distinct executedBy)
sum(field), avg(field)The total, or the average, of a number or durationavg(duration)
min(field), max(field)The smallest, or the largest, valueNumbers, durations and dates.
passRate()The percentage of passed resultsResults and runs only. From 0 to 100, rounded to one decimal. Empty when nothing executed.
bucket(dateField, unit)The start of the time bucket that a date falls inUnits: hour, day, week, month, quarter, year. Weeks start on Monday.

passRate() counts the following:

  • On results, passed results divided by all results.
  • On runs, passed cases divided by passed, failed, blocked and skipped cases. This is the same figure as runs.passRate.
SELECT bucket(executed, week) AS period, passRate() AS rate, count() AS results FROM results GROUP BY 1 ORDER BY 1

Time and time zones

Dates and relative times are read in the Timezone of your profile. With Auto-detected, UTC is used. now, startOf, endOf, bucket and the date comparisons all follow this zone.

Relative time accepts every unit: m, h, d, w, mo, q and y. Fixed-duration fields accept m, h, d and w.

Aggregation rules

  • GROUP BY takes fields, expressions and positions, such as GROUP BY 1. It does not take a SELECT alias. Repeat the expression, or use its position.
  • HAVING filters groups with aggregates, such as HAVING count() > 5. It does not take aliases, so HAVING n > 5 fails.
  • ORDER BY takes aliases (ORDER BY n DESC), positions counted from 1 (ORDER BY 2) and aggregates (ORDER BY count() DESC). Aggregates need a group or an aggregate in SELECT. User and tag fields cannot be sorted.
  • Grouping works on number, boolean, enum, option, user, reference and tag fields. Text and dates cannot be grouped directly. Group a date with bucket().
  • Tags and multi-select fields count a row in the group of each of its values. Two such fields grouped together give one group per combination, so the groups can add up to more than the number of rows.
  • No value. Rows without a value form a group named No value. It sorts first.
  • count(field) counts the rows where the field has a value.
  • HAVING on a tag or multi-select field is refused. Filter in WHERE instead.
  • Without GROUP BY, an aggregate returns one row for all matching rows.
  • Time buckets start at the start of the unit in your time zone. Empty buckets are not returned.
  • Date pickers compare by local day.
  • Groups are shown up to 1,000 per page. The result says so when there are more.

History

Change history records changes to some fields. Two operators read it:

  • status was in_progress is true when the value is the current value, or was one of the values recorded in the history.
  • status changed is true when the history holds at least one change. Add modifiers to narrow it:
ModifierMeaning
from vThe change started from v.
to vThe change ended at v.
by userThe change was made by currentUser(), or by a member of membersOf(...).
after tThe change happened after t.
before tThe change happened before t.

All the modifiers you write must match the same change. t is a date or a relative time, read in your time zone.

FROM defects WHERE status changed from resolved to open after now(-14d)
FROM cases WHERE priority changed by currentUser() after now(-7d)

Fields with history

EntityFields
casespriority, severity, status, type, layer, behavior
defectsstatus, assignee, severity, priority

Change history is not available for run state, milestone and suite moves, custom fields (cf[...]), or linked fields. A query that uses them returns history_not_available.

Rules:

  • Use was and changed in WHERE. In HAVING they return history_placement.
  • Change history keeps records for your plan’s history retention window. See Data retention.
  • Changes made before the record was audited are not known, so a value can be missing from its history.

In query() references

id in query("Saved name") keeps the rows that a saved query returns:

FROM cases WHERE id in query("Critical failures")
  • The saved query must return the same entity, and in rows mode.
  • Its ORDER BY and LIMIT are ignored.
  • A saved query can refer to another query up to three levels deep. A cycle is refused.
  • You can refer only to queries you can see. An unknown or hidden name returns unknown_saved_query.

Archived cases

A query over cases leaves out archived cases unless the query names archived itself. The same rule applies to a related-record condition over cases, such as exists(cases ...).

FROM cases WHERE title ~ "login" -- archived cases are left out
FROM cases WHERE archived = true -- the condition decides

A query over results is different. Archiving keeps a case’s execution history, so FROM results (and FROM runCases) includes the results of archived cases. To leave them out, name the case as live:

FROM results WHERE case.archived = false -- the results of archived cases are left out

Limits

LimitValue
Query text4,000 characters
Values in one in (...) list200
LIMIT (rows per page)50 by default, 500 at most
OFFSET10,000 at most
Rows countedExact up to 10,000, then 10,000+
Groups per page1,000 at most
Relation nesting (exists and count)Two levels
in query() referencesThree levels. Cycles are refused.
CSV export10,000 rows, or 1,000 groups for an aggregate query
Saved query name120 characters, unique in the organization
Saved query description2,000 characters

Errors

Each error has a code, a message, and the place in the query where it occurs. Through the API, each error appears in details.errors with the same code. See Errors.

Syntax errors

CodeWhat it meansWhat to do
unexpected_characterThe query has a character PQL does not use, such as #.Remove it, or put it in quotes if it is text.
unterminated_stringA quote is not closed.Add the closing quote.
missing_fromThe query does not start with FROM.Start the query with FROM and an entity.
unexpected_tokenA word or symbol is in the wrong place. A clause repeated twice, or a decimal LIMIT or OFFSET, shows this too.Check the order of the clauses, and use whole numbers.
unexpected_end_of_queryThe query ends too early, often after AND.Finish the condition.
expected_expressionA value is missing, as in in ().Add the value or the field.
unclosed_parenthesisA ( has no matching ).Add the ).
nesting_too_deepThe parentheses are nested too deeply.Simplify the condition.
invalid_durationA duration uses a unit PQL does not have, such as 3x.Use m, h, d, w, mo, q or y.
number_out_of_rangeA number is too large or not finite.Use a smaller number.
query_too_longThe query is longer than 4,000 characters.Shorten it, or move part of it into a saved query.

Names and functions

CodeWhat it meansWhat to do
unknown_entityThe name after FROM is not an entity.Use one of the nine entities.
unknown_fieldThe field is not on this entity. The message can suggest the closest name.Check the entity’s table in Entities.
unknown_custom_fieldNo custom field of this entity has the title.Check the title in your custom field settings.
unknown_functionThe name is not a PQL function.Check the functions table.
not_a_functionA field name was written as a function.Remove the parentheses.
function_arityThe function has the wrong number of arguments.Check the function’s form in the table.
invalid_argumentAn argument has the wrong form, such as a startOf offset with no unit.Use a signed duration, such as -1mo.
function_not_availableThe function does not apply to this entity, such as passRate() on cases.Use passRate() on results or runs.
unknown_time_unitThe time unit is not one PQL knows.Use hour, day, week, month, quarter or year.
unknown_groupNo group in the organization has this name.Check the group name.
set_function_placementmembersOf(), activeMembers() or formerMembers() is used outside in (...).Put it inside in (...) on a user field.
unknown_relationThe relation is not on this entity.Check the relations table.
relation_nesting_too_deepSub-queries nest more than two levels deep.Flatten the query.
relation_count_placementcount(relation) is used without a comparison.Compare it with a number, such as count(results) >= 1.
relation_placementexists is used in HAVING.Move it to WHERE.
unknown_saved_queryNo saved query you can see has this name.Check the name. You see only the queries you can access.
history_not_availableChange history is not kept for this field.See Fields with history.
history_placementwas or changed is used in HAVING.Move it to WHERE.

Values and operators

CodeWhat it meansWhat to do
operator_not_allowedThe operator does not apply to this field.Check the operators table.
not_nullableis empty is used on a field that always has a value.Remove it.
type_mismatchThe value’s type does not match the field, such as text for a number.Use a value of the right type.
left_side_must_be_fieldThe left side of a comparison is not a field.Put the field on the left.
not_booleanA field is used as a condition but is not a yes-or-no field.Compare it, such as archived = true.
unquoted_text_valueA text value has no quotes.Put it in quotes.
unknown_valueThe value is not one of the options, members or names the field allows.Use one of the values the message lists.
invalid_date_formatA date is not YYYY-MM-DD or YYYY-MM-DD HH:mm.Write the date in one of those forms.
invalid_dateThe date does not exist, such as 2026-02-30.Use a real date.
relative_duration_in_dateA duration is used as a date, such as 7d.Use a time function, such as now(-7d).
invalid_duration_unitA month, quarter or year is used on a fixed-duration field.Use m, h, d or w on duration fields.
in_list_too_longAn in (...) list has more than 200 values.Split the list.

Aggregates, grouping and paging

CodeWhat it meansWhat to do
aggregate_not_allowedAn aggregate is used in WHERE or GROUP BY.Move it to SELECT or HAVING.
not_aggregatableThe function does not apply to this field, such as sum(title) or avg(passRate).Use a number, duration or date, or passRate().
not_groupableThe field cannot be grouped, such as text.Group by another field, or use bucket() for a date.
not_groupedA SELECT item is neither grouped nor aggregated.Add it to GROUP BY, or wrap it in an aggregate.
group_by_requires_selectGROUP BY has no SELECT list.Name the grouped fields and aggregates in SELECT.
having_requires_group_byHAVING is used with no group and no aggregate.Add GROUP BY, or an aggregate in SELECT.
multi_valued_havingHAVING filters a tag or multi-select field.Filter in WHERE.
order_requires_group_byORDER BY sorts by an aggregate with no group.Add GROUP BY, or an aggregate in SELECT.
position_out_of_rangeA position does not match a SELECT item.Use a number from 1 to the number of SELECT items.
not_sortableThe field cannot be sorted, such as a user or tag field.Sort by another field.
invalid_select_itemA SELECT item is neither a field nor an aggregate.Use a field, or an aggregate such as count().
invalid_bucketbucket() has no date field, or no unit.Use bucket(created, day).
duplicate_aliasThe alias is already used in SELECT.Choose another name.
limit_out_of_rangeLIMIT is outside 1 to 500.Use a number from 1 to 500.
offset_out_of_rangeOFFSET is above 10,000.Use a number up to 10,000.

When the server does not support a construct yet, the query returns Not available yet in the editor, with the construct named. Remove that part of the query for now.

Worked examples

Each example answers one common question. Replace the names with your own suites, milestones and environments.

QuestionQuery
Which cases failed in their latest run on staging?FROM cases WHERE lastResult(environment = "staging").status = failed ORDER BY title
Which critical cases failed last on staging?FROM cases WHERE priority = "Critical" AND lastResult(environment = "staging").status = failed ORDER BY title
Which defects were reopened in the last 14 days?FROM defects WHERE status changed from resolved to open after now(-14d)
Which defects assigned to me were reopened in 30 days?FROM defects WHERE assignee = currentUser() AND status changed from resolved to open after now(-30d)
Which flaky cases have no pass in the last 30 days?FROM cases WHERE isFlaky = true AND NOT exists(results where status = passed AND executed > now(-30d))
Which cases have never been executed?FROM cases WHERE NOT exists(results)
What is the pass rate per week?SELECT bucket(executed, week) AS period, passRate() AS rate, count() AS results FROM results GROUP BY 1 ORDER BY 1
How are this month’s results split by status?SELECT status, count() AS n FROM results WHERE executed >= startOf(month) GROUP BY status ORDER BY n DESC
Which open defects are there by severity?SELECT severity, count() AS n FROM defects WHERE status != closed GROUP BY severity ORDER BY n DESC
How many failed runs did each environment have last month?SELECT environment, count() AS n FROM runs WHERE status = failed AND closed > startOf(month, -1mo) AND closed < startOf(month) GROUP BY environment
Who executed the most results this week?SELECT executedBy, count() AS n FROM results WHERE executed > now(-7d) GROUP BY executedBy ORDER BY n DESC
Which runs closed this quarter have the best pass rate?FROM runs WHERE state = closed AND closed > startOf(quarter) ORDER BY passRate DESC
Which cases in the Checkout tree changed priority this week?FROM cases WHERE suite under "Checkout" AND priority changed after startOf(week)
Which cases have Risk set to High in the Login suite?FROM cases WHERE cf["Risk"] = "High" AND suite under "Login"
Which milestones are active and past their due date?FROM milestones WHERE status = active AND due < now()

Next step

  • Write and run queries in Queries.
  • Put a saved query on a dashboard in Dashboards.
  • Send queries from code with the PQL API.