The custom_field_query filter lets you search documents by custom field values using a structured boolean expression. Paperless-ngx parses the expression serverside — pypaperless provides a small builder so you never have to construct the JSON by hand.
| Class | Operator shortcut | JSON output |
|---|---|---|
CustomFieldQuery(field, op, value) |
— | [field, op, value] |
CustomFieldQueryAnd(q1, q2, …) |
q1 & q2 |
["AND", [q1, q2, …]] |
CustomFieldQueryOr(q1, q2, …) |
q1 | q2 |
["OR", [q1, q2, …]] |
CustomFieldQueryNot(q) |
~q |
["NOT", q] |
Import from pypaperless.models.custom_field_query (or via pypaperless.models.types):
from pypaperless.models.custom_field_query import CustomFieldQueryBuild an expression and pass str(q) to the custom_field_query kwarg of documents.filter():
from pypaperless.models.custom_field_query import CustomFieldQuery
q = CustomFieldQuery("Status", "exact", "open")
async with paperless.documents.filter(custom_field_query=str(q)) as docs:
async for doc in docs:
print(doc.title)Use &, | and ~ to build boolean queries. Chained & and | are automatically flattened:
q = (
CustomFieldQuery("Status", "exact", "open")
& CustomFieldQuery("Amount", "gte", 100)
& ~CustomFieldQuery("Archived", "exact", True)
)
# Serialises to:
# ["AND", [["Status","exact","open"], ["Amount","gte",100], ["NOT",["Archived","exact",true]]]]OR across multiple categories:
q = (
CustomFieldQuery("Category", "exact", "A")
| CustomFieldQuery("Category", "exact", "B")
| CustomFieldQuery("Category", "exact", "C")
)field can be either the integer ID or the name string:
CustomFieldQuery(42, "exists", True) # by ID
CustomFieldQuery("Invoice Amount", "gte", 100) # by nameThe valid operators depend on the field's data type:
| Applies to | Operators |
|---|---|
| All types | exact in isnull exists |
STRING LONGTEXT URL MONETARY |
icontains istartswith iendswith |
INTEGER FLOAT MONETARY DATE |
gt gte lt lte range |
DOCUMENT_LINK |
contains |
DATE (component) |
year__exact month__exact day__exact etc. |
exists is the idiomatic way to check field presence:
CustomFieldQuery("Due Date", "exists", True) # document has the field
CustomFieldQuery("Due Date", "exists", False) # document does not have the fieldPass a list as the value:
CustomFieldQuery("Status", "in", ["open", "pending"])
CustomFieldQuery("Amount", "range", [10, 100]) # start, end (inclusive)CustomFieldQuery("Invoice Date", "year__exact", 2024)
CustomFieldQuery("Invoice Date", "month__exact", 12)str(q) is shorthand for json.dumps(q.build()). Call build() directly if you need the raw Python structure:
q = CustomFieldQuery("Amount", "gte", 50) & CustomFieldQuery("Status", "exact", "open")
print(q.build())
# ["AND", [["Amount", "gte", 50], ["Status", "exact", "open"]]]
print(str(q))
# '["AND", [["Amount", "gte", 50], ["Status", "exact", "open"]]]'Paperless-ngx enforces:
- Maximum nesting depth: 10
- Maximum number of atoms: 20
Exceeding these limits returns a HTTP 400 validation error.