Skip to content
Doug Blank edited this page Sep 17, 2026 · 7 revisions

GOQL

Every list view in Gramps Connect — People, Families, Events, and so on — has a search box. By default, typing into it does a fast plain-text match against that view's handful of common fields (for People: Gramps ID and name; for Place: ID, title, and place name; and so on) — enough for most everyday searches, and the default because it's simpler than a query language.

Next to the box is a checkbox: "Use Gramps Object Query Language." Turn it on and the same box instead takes a full GOQL (gramps-object-query-language) condition — a small language built specifically for querying Gramps records, capable of things plain-text matching can't touch at all: reaching across relationships (a join, in database terms — "every family whose mother and father share a surname"), and even reaching backward across them ("every note nothing points to any more"). This page is for anyone who wants to write one of those conditions — no programming experience needed.

GOQL is also what powers Gramplets' filter()/db. get_number_of() calls and the where= argument to people()/ families()/etc. — the same syntax you'd type into the search box with the checkbox on also works from inside an add-on's Python code. (db. get_number_of()/the older bare count() are Gramplet-API functions for counting matching records, not GOQL's own any/len covered below — different thing, same name.)

Don't want to write it by hand at all? The "Filters" button next to each list's title offers dozens of common rules as plain checkboxes — combinable with AND/OR, nestable, and negatable — and builds the exact same GOQL underneath, shown read-only so you can see what it wrote. See the Overview page's feature list for that. Its "+ New custom rule…" is the one spot inside that dialog that still takes a raw GOQL condition by hand, same syntax as this whole page — once saved and named, it's reused as an ordinary checkbox from then on, never retyped.

The basic idea

A query is always written in the context of a view — Person, Family, Event, and so on — and is just one condition:

surname == 'Smith'

This reads as: when in the Person view, find records where the surname equals 'Smith'. The view you're searching in fixes which kind of record the condition applies to.

A few symbols come up over and over:

Symbol Means
==, != is equal to / is not equal to
<, <=, >, >= is less than, at most, more than, at least (earlier/later, smaller/bigger)
and, or, not combine conditions, or flip one around
in [ ... ] matches any one of a list of values
'text' in field matches if field contains 'text' anywhere in it
like(field, 'pattern') matches a text pattern, where % stands for "anything"
regex(field, 'pattern') matches a regular expression, for those who already know them

Text values go in single quotes ('Smith'); numbers don't (1968).

Combining conditions

and requires both sides to be true; or needs only one. Both read naturally: gender == Person.MALE and surname == 'Smith' finds every man named Smith, while given_name == 'John' or surname == 'Doyle' finds everyone named John or anyone at all named Doyle. Mixing the two works the same as ordinary arithmetic, where multiplication happens before addition — and is checked before or — but parentheses make the intent clear regardless: (gender == Person.MALE and surname == 'Smith') or given_name == 'Mary' finds every male Smith, plus anyone named Mary of any gender.

not flips a condition around, matching whenever the thing inside it isn't true: not (surname == 'Smith') is everyone whose last name isn't Smith. Comparisons can also be chained, the way real Python allows — Date('Jan 1, 1900') < birth.date.sortval < Date('Jan 1, 1950') finds everyone born strictly between those two dates, in one line instead of two joined with and.

Matching text

Beyond a plain ==, a few looser ways to match text cover most everyday searches. given_name in ['John', 'Jane'] matches any name in the list — add as many as you like. 'an' in given_name matches anywhere in the name (Jane, Alexander, Susan, ...) with no wildcards needed. like(field, 'J%') matches a pattern where % stands for "anything," so like (given_name, 'J%') matches John, Jane, James, and so on. For anyone already comfortable with regular expressions, regex(field, 'pattern') is more powerful still — regex(surname, '^[SD]') finds every Smith and Doyle in one go, something like(...) can't express without writing out a separate condition for each starting letter.

Reaching related records

A field doesn't have to live directly on the record you're searching. birth and death reach from a person to their birth or death event, so birth.date.sortval is the date of that event, and birth.place.title follows it one step further, to that event's place. The same idea works across other record types: father and mother reach from a family to each parent's own Person record (father.surname == 'Smith'), source reaches from a citation to what it cites, and enclosed_by reaches from a place to the place that encloses it (a city's county, say — and it chains with itself, so enclosed_by.enclosed_by.title reaches two levels up).

Both sides of a comparison can be one of these paths, not just a fixed value, which is what makes a query like "families where the mother and father share a last name" possible: father.surname == mother.surname. Chaining and comparing can combine freely — father.birth.date.sortval < Date('Jan 1, 1850') reaches two steps from a family (to the father, then to his birth event) to find older generations without knowing who they are ahead of time.

There's no limit to how many of these hops one path can cross, and each hop can land on a different kind of record entirely. Starting in the Family view, father.birth.place.title == 'Chicago, Cook, Illinois, USA' reaches from the family to the father's own Person record, from there to his birth Event, and from there to that event's Place — three hops through three different kinds of record, ending at the place's name, all in one line. The same chaining works from any view, in whatever combination the relationships above allow.

Collections: any and len

A person has exactly one birth event, but any number of children, notes, citations, or attached media — those need a different kind of check. any(c.given_name == 'Steve' for c in children) matches a family if any child satisfies the condition; leaving the condition out (any(n for n in notes)) just asks whether anything is attached at all, which is how not any(n for n in notes) finds people with no notes recorded. len(...) asks "how many" instead of "at least one" — len([c for c in children]) > 2 finds families with more than two children, and len([c for c in children if c.gender == Person.MALE]) > 1 narrows that to counting only the sons.

A collection is as far as a single query can reach in that direction, though. The chaining described above (father.birth.place.title) only works through relationships that connect to exactly one record — any/len can tell you whether something inside a collection matches a condition, but the query can't then keep chaining past that match to reach one of its own related records. A family's father has his own parent family, for instance — but since a person can be recorded as a child of more than one family, that link is a collection too, one step further than a query can currently follow. So "every family whose father and grandfather were born in the same county" isn't something GOQL can express yet.

Reverse references: backlinks

Every collection above reaches outward — a family's own children, a person's own notes. backlinks is the one collection that reaches the other way: does anything else in the tree point to this record? It's available from every one of the ten record types, even ones with no outward collections of their own (a Tag has no children or notes of its own, but plenty of records can be tagged, so Tag's own backlinks condition finds tags nothing uses):

not any(bl for bl in backlinks)                                    # nothing references this record at all
any(bl._class == 'Person' for bl in backlinks)                     # at least one Person references it
any(bl for bl in backlinks) and len([bl for bl in backlinks]) > 1  # referenced by more than one thing

This is what makes a query like "every source nobody cites any more" or "every place with no events recorded at it" possible — not any(bl for bl in backlinks), run in the Source or Place view.

_class is the one field a backlinks condition can test — the referrer's own record type ('Person', 'Family', ...). Since a backlink's referrer can be any of the ten record types at once, _class can't be followed any deeper the way a normal relationship can — there's no _class.primary_name — but it does accept in/not in against a list:

any(bl._class in ['Person', 'Family'] for bl in backlinks)
any(bl._class != 'Media' for bl in backlinks)

len([bl for bl in backlinks]) works exactly like len(...) above.

Dates

Date('...') understands ordinary date text, so birth.date.sortval >= Date('Jan 1, 1968') finds everyone born on or after that day. One thing worth knowing: sortval is always a single point in time, with no "about," "before," or "estimated" qualifier attached — a birth date entered as "before 1968" has the same sortval as plain "Jan 1, 1968," so a >= comparison would count that person as born on or after the cutoff even though "before" means the opposite. When that distinction matters, compare birth.date.modifier against a named constant instead, such as Date.MOD_ABOUT.

Named constants

Some fields, like a person's gender or a citation's confidence, compare against a named constant rather than a raw number — gender == Person.MALE or confidence >= Citation.CONF_HIGH — pulled straight from Gramps itself so they never drift out of sync with what Gramps actually stores.

Things GOQL can't do

GOQL is a small, fixed set of building blocks, not a full programming language — anything outside the patterns above is rejected with an error rather than guessed at.

Learning more

This page covers the everyday patterns; the search box's own "i" button, next to each view's search field, lists the exact fields and relationships available for that view. For the complete syntax reference — every operator, relationship, collection, and edge case, plus the reasoning behind each — see where_expr.md in the gramps-object-query-language repository, the project GOQL is built on.

Clone this wiki locally