Skip to content

Query Language

Thogits uses a JSON-based query language for filtering thogits and tags. Filters are passed as the filter field in search and bulk operation requests.

Every filter is a single JSON object with exactly one key that determines its type.


Combine filters using and, or, and not.

{ "and": [filter, filter, ...] }
{ "or": [filter, filter, ...] }
{ "not": filter }

and requires all children to match. or requires at least one. not inverts.

Combinators nest arbitrarily:

{
"and": [
{ "has_tag": "Task" },
{ "or": [
{ "Task.priority": { "gte": 8 } },
{ "not": { "Task.status": { "eq": "Backlog" } } }
]}
]
}
{ "search": "keyword" }

Case-insensitive substring match against both name and description. Matches if either contains the search text.

{ "has_tag": "Task" }

Matches thogits that have the specified tag applied. The value can be a tag name or a tag ULID.

Tag inheritance is respected: if tag B extends tag A, a thogit with tag B also satisfies { "has_tag": "A" }.

{ "name": <TextFilter> }

Filters by the thogit’s name. See Text Filters below.

{ "description": <TextFilter> }

Filters by the thogit’s description. If the thogit has no description, the filter does not match (except with not).

{ "Tag.fieldName": <FieldFilter> }

Access a tag’s field using TagName.fieldName or TagULID.fieldName. Tag names and ULIDs are interchangeable wherever a tag is referenced.

{ "Task.priority": { "gt": 5 } }
{ "Project.deadline": { "lt": "2025-06-01" } }
{ "Bug.resolved": true }

If the thogit does not have the specified tag or field, the field value is treated as null.

Follow Reference fields to filter on properties of related thogits using -> syntax:

{ "Task.projectRef->Project.priority": { "gt": 5 } }

This reads: “the thogit referenced by Task.projectRef must have Project.priority > 5.”

Chain multiple hops with ->:

{ "Task.projectRef->Project.orgRef->Org.tier": { "eq": "Enterprise" } }

Maximum depth: 5 hops. Exceeding this returns an error.

The final segment after the last -> can be:

TerminalExample
Tag.field...->Project.priority": { "gt": 5 }
name...->name": { "regex": "^Acme" }
description...->description": { "eq": "..." }
has_tag...->has_tag": "Enterprise"

Every non-terminal segment must be Tag.refField format (a Reference-type field).


Used by name, description, and string field filters.

{ "regex": "^Feature.*v2" }

Rust-flavour regular expression. Matches if the pattern matches anywhere in the string.

{ "eq": "exact value" }
{ "neq": "not this" }
{ "gt": "b" }
{ "gte": "b" }
{ "lt": "m" }
{ "lte": "m" }

String comparisons use lexicographic ordering.


The filter syntax depends on the field’s schema type. Field filters also support shorthand bare values.

Six operators are available for ordered comparisons:

OperatorMeaning
eqEqual
neqNot equal
gtGreater than
gteGreater than or equal
ltLess than
lteLess than or equal

All text filter forms:

{ "Tag.title": { "eq": "exact" } }
{ "Tag.title": { "regex": "pattern" } }
{ "Tag.title": { "gt": "a" } }

Ordinal operators with numeric values:

{ "Tag.priority": { "gt": 5 } }
{ "Tag.count": { "eq": 10 } }
{ "Tag.score": { "lte": 99.5 } }

Direct boolean value (shorthand for eq):

{ "Tag.isActive": true }
{ "Tag.isActive": false }

Or using eq/neq:

{ "Tag.isActive": { "eq": true } }
{ "Tag.isActive": { "neq": true } }

eq matches only the exact boolean value. neq matches the opposite value and null/missing fields — so { "neq": true } matches thogits where the field is false or not set at all.

Ordinal operators with ISO 8601 date strings. Two formats are accepted:

  • Date only: YYYY-MM-DD (treated as midnight T00:00:00)
  • Date-time: YYYY-MM-DDTHH:MM:SS
{ "Tag.deadline": { "lt": "2025-06-01" } }
{ "Tag.createdAt": { "gte": "2025-01-15T10:30:00" } }

Date detection is automatic: if the string value parses as a date, it’s compared as a date. Otherwise it falls back to text comparison.

Select fields store values as {"variant": "Name"}. Filtering compares by ordinal position in the schema’s variant list when using ordinal operators, and by variant name when using regex.

{ "Tag.status": { "eq": "Done" } }
{ "Tag.priority": { "gt": "Low" } }
{ "Tag.status": { "regex": "In.*" } }

With ordinal comparison, if the schema defines variants ["Low", "Medium", "High"], then { "gt": "Low" } matches "Medium" and "High" (positions 1 and 2 are greater than position 0).

Same operators as Select. Matches if any selected variant satisfies the condition.

{ "Tag.labels": { "eq": "urgent" } }
{ "Tag.labels": { "in": ["urgent", "important"] } }

Reference fields store a ULID string pointing to another thogit. Direct filtering is limited to existence checks:

{ "Tag.projectRef": { "exists": true } }
{ "Tag.projectRef": { "exists": false } }

For filtering on the referenced thogit’s properties, use reference traversal.

Check whether a field has a value:

{ "Tag.assignee": { "exists": true } }
{ "Tag.assignee": { "exists": false } }

Check membership in a set of values. Works with Select and MultiSelect fields:

{ "Tag.status": { "in": ["Open", "In Progress"] } }

Matches if the field value (or any selected variant for MultiSelect) appears in the array.

Field filters accept bare values as shorthand for { "eq": value }:

Bare ValueEquivalent
null{ "exists": false }
true / falseBoolean match
42 (number){ "eq": 42 }
"text" (string){ "eq": "text" }
{ "Tag.priority": 5 }
{ "Tag.active": true }
{ "Tag.notes": null }

Tag filters are used on the tag search endpoint (POST /api/tags/search). They support a subset of the thogit filter language.

{ "and": [filter, filter, ...] }
{ "or": [filter, filter, ...] }
{ "not": filter }
{ "search": "keyword" }
{ "name": <TextFilter> }
{ "description": <TextFilter> }

search is case-insensitive substring match on both the tag’s name and description.


POST /api/thogits/search
{ "filter": { "has_tag": "Task" } }

Omitting filter or passing null returns all thogits.

POST /api/tags/search
{ "filter": { "name": { "regex": "^Project" } } }
POST /api/thogits/bulk-delete
{
"filter": { "has_tag": "Archived" },
"dry_run": true
}

Returns { matched_count, affected_count, affected_ids, dry_run }. Set dry_run: true to preview without deleting.

POST /api/thogits/bulk-apply-tag
{
"filter": { "has_tag": "Task" },
"tag": { "tag_ref": { "Existing": "<tag-ulid>" }, "field_values": {} },
"dry_run": false
}
POST /api/thogits/bulk-remove-tag
{
"filter": { "Task.status": { "eq": "Done" } },
"tag_id": "<tag-ulid>",
"dry_run": false
}
POST /api/thogits/bulk-update-fields
{
"filter": { "has_tag": "Task" },
"tag_id": "<tag-ulid>",
"field_values": { "priority": 1 },
"merge": true,
"dry_run": false
}

When creating a new thogit within a filtered view, the system extracts sensible defaults from the active filter. For example, a filter { "has_tag": "Task" } will pre-apply the “Task” tag. A filter { "Task.priority": { "eq": 5 } } will pre-set priority to 5.

Negations (not, neq, exists: false) are ignored during extraction since they don’t imply a positive value. For or filters, the first branch with extractable values is used.


ConditionError
Empty filter object {}"Filter object cannot be empty"
Unknown filter key"Unknown filter. Expected: and, or, not, search, has_tag, name, description, or Tag.field"
Tag not found"Tag 'X' not found"
Traversal exceeds 5 hops"Reference traversal exceeds max depth of 5 hops"
Invalid dot-notation"Invalid dot-notation: 'X'"
Invalid operator value type"'gt' requires a number, string, or date"

All high-priority tasks not yet done:

{
"and": [
{ "has_tag": "Task" },
{ "Task.priority": { "gte": 8 } },
{ "not": { "Task.status": { "eq": "Done" } } }
]
}

Thogits whose name starts with “RFC” or that have a description mentioning “proposal”:

{
"or": [
{ "name": { "regex": "^RFC" } },
{ "description": { "regex": "(?i)proposal" } }
]
}

Tasks assigned to projects in the Enterprise tier (reference traversal):

{ "Task.projectRef->Project.orgRef->Org.tier": { "eq": "Enterprise" } }

Thogits with any value in the “assignee” field:

{ "Task.assignee": { "exists": true } }

Bulk preview: which thogits would be deleted?

{
"filter": {
"and": [
{ "has_tag": "Temp" },
{ "Temp.createdAt": { "lt": "2025-01-01" } }
]
},
"dry_run": true
}