Skip to content

Finders

The Flywheel SDK exposes collections of containers (projects, subjects, sessions, acquisitions, analyses, jobs, files) as Finder objects. A Finder wraps the underlying paginated API and provides a consistent interface for filtering, sorting, and iterating over results — regardless of which collection you are querying.

You do not instantiate Finder directly.

Finder objects on the client and containers
# Client-level finders span the entire instance
fw.groups
fw.projects
fw.subjects
fw.sessions
fw.acquisitions
fw.analyses
fw.files
fw.jobs
fw.gears

# Container-level finders scope to that container's children
group.projects
project.subjects
project.sessions
subject.sessions
session.acquisitions

Note

container.files and container.analyses are not Finders — they are plain lists populated when the container was fetched. Use fw.files or fw.analyses (client-level) for filtered, paginated queries across the instance, or reload the container to refresh its .files and .analyses lists.

See Getting Started for client initialization and Core Concepts: Containers for the full container type reference.

Method Reference

The Finder interface exposes five methods. Choose based on how many results you expect and whether you want them all in memory at once.

Method Return type Zero results Multiple results Notes
find() list Empty list All matches Loads all matching results into memory
find_first() object or None Returns None Returns first match Fetches at most 1 result from the API
find_one() object Raises ValueError Raises ValueError Fetches at most 2 results to detect ambiguity
iter() iterator Empty iterator All results (lazy) Pages through all items with no filter
iter_find() iterator Empty iterator All matches (lazy) Pages through filtered results lazily

find()

Returns a list of all matching objects. When no limit is specified, find() internally delegates to iter_find() and collects all results into a list. When limit is specified, it fetches that page and returns it directly.

Find containers with and without a limit
# All projects (no limit — collects everything)
projects = fw.projects.find()

# All sessions created after a date
sessions = project.sessions.find("created>2023-01-01")

# First 100 sessions (explicit limit returns a single page)
sessions = project.sessions.find(limit=100)

find_first()

Returns the first matching object, or None if nothing matched. Always sends limit=1 to the API, making it the most efficient choice when you only need one result and absence is acceptable.

Find the first matching session
session = subject.sessions.find_first("label=baseline")
if session is None:
    print("No baseline session found")

find_one()

Returns the single matching object. Raises ValueError if zero or more than one result matches. Internally requests limit=2 so it can detect the ambiguous case without fetching the entire result set.

Use find_one() when exactly one result is a correctness requirement — for instance, looking up a subject by a unique identifier.

Find exactly one matching subject
try:
    subject = project.subjects.find_one("label=sub-001")
except ValueError as e:
    print(f"Lookup failed: {e}")

iter()

Iterates over every item in the collection without a filter. Pages through results lazily using cursor-based pagination (after_id), so it does not load everything into memory at once. Default page size is 250.

Iterate over all sessions in a project
for session in project.sessions.iter():
    print(session.label)

iter_find()

Iterates over all items matching a filter. When no sort is provided, it uses cursor-based pagination (after_id). When sort is provided, it falls back to page-number pagination. Default page size is 250.

Iterate over all failed jobs
for job in fw.jobs.iter_find("state=failed"):
    print(f"Failed job: {job.id}")

Filter Syntax

Filter expressions are strings passed as the first positional argument to any finder method. Multiple conditions separated by commas are combined with AND logic.

"field=value"
"field1=value1,field2=value2"

Field references

Use dot notation to filter on nested fields.

Filter on top-level and nested fields
# Top-level field
fw.projects.find("label=My Project")

# Nested field
fw.sessions.find("subject.label=sub-01")

# Deeply nested
fw.files.find("parents.project=abc123,parent_ref.type=acquisition")

Value types

The API automatically converts filter values to the appropriate type:

Format Type
YYYY-MM-DD Date
YYYY-MM-DDTHH:mm:ss Timestamp
Numeric (e.g., 42, 15.7) Number
null Null / missing
Quoted string (e.g., "My Label") String literal

Wrap values in double quotes when they contain spaces or when you need to force string interpretation.

Quote string values with spaces
# String with spaces — quotes required
project = fw.projects.find_first('label="Anxiety Study"')

# Force string interpretation of a numeric-looking label
subject = project.subjects.find_first('label="123456"')

Operator reference

Operator Meaning Example
= Equals label=baseline
!= Not equals label!=excluded
< Less than age<65
<= Less than or equal age<=65
> Greater than created>2023-01-01
>= Greater than or equal created>=2023-01-01
=~ Regex match label=~.*_v2$
=| In list gear_info.name=|[dcm2niix,file-metadata-importer]
!=| Not in list state!=|[failed,cancelled]

Regex notes: The =~ operator does not support quoted values. Use unquoted patterns.

Filter with regex operators
# Match acquisitions whose labels end in _v2
fw.acquisitions.find("label=~.*_v2$")

# Match DICOM files by extension
list(fw.acquisitions.iter_find("files.name=~.*\\.dcm"))

List operator syntax: Provide the values as a bracket-delimited, comma-separated list with no spaces.

Filter using the in-list operator
# Jobs running either of two gears
list(fw.jobs.iter_find("gear_info.name=|[dcm2niix,file-metadata-importer]"))

Multiple conditions

Comma-separated conditions are ANDed together. There is no OR operator at the filter string level — use =| (in-list) for OR on a single field, or issue multiple queries.

Combine multiple filter conditions
# Sessions in one project that are labeled baseline and belong to male subjects
sessions = fw.sessions.find("project.label=MyProject,label=baseline,subject.sex=M")

# Subjects who are human and have completed a session after a date
subjects = project.subjects.find("type=human,modified>2023-06-01")

Sorting

Pass sort as a keyword argument. The format is fieldname:direction where direction is asc or desc. Separate multiple sort fields with commas.

Sort finder results
# Most recently created sessions first
sessions = project.sessions.find("label=baseline", sort="created:desc")

# Subjects sorted by label ascending
subjects = project.subjects.find(sort="label:asc")

# Multiple sort fields
sessions = project.sessions.find(sort="subject.label:asc,created:desc")

Constraint: sort cannot be combined with after_id cursor-based pagination. If you pass both sort and after_id, the request will fail or produce undefined results. When iter_find() detects a sort argument, it automatically falls back to page-number pagination to avoid this conflict.

Pagination

Parameters

Parameter Type Meaning
limit int Maximum number of results per page (or total, when using find() with an explicit limit)
page int 1-based page number for offset pagination
skip int Number of results to skip before returning
after_id str ID of the last item from the previous page (cursor pagination)

Cursor pagination vs. offset pagination

Cursor pagination (after_id) is the default for iter() and iter_find() (when no sort is given). The client reads the last item's ID from each page and passes it as after_id to the next request. This approach is stable — if items are added or removed between pages, earlier pages are not shifted. The trade-off is that you cannot jump to an arbitrary page, and you cannot combine it with sort.

Offset pagination (page / skip) lets you jump to any page and is compatible with sort. The trade-off is that inserts and deletes during iteration can cause duplicates or skipped items.

Manual offset pagination in batches of 200
limit = 200
page = 1
results = []

while True:
    batch = project.sessions.find(limit=limit, page=page)
    results.extend(batch)
    if len(batch) < limit:
        break
    page += 1

iter_find() handles this loop automatically. Use manual pagination only when you need direct page access or when iter_find() does not fit your control flow.

Page size and throughput

The default page size for iter() and iter_find() is 250. For bulk export or large data migrations, increasing limit to 400–800 typically gives better throughput by reducing the number of round trips. Values above 1 000 are likely to cause server-side timeouts or oversized responses.

Use a larger page size for bulk processing
for session in project.sessions.iter_find("label!=test", limit=500):
    process(session)

Memory vs. Lazy Iteration

find() with no limit collects all matching results into a Python list before returning. For small collections this is convenient. For large collections (tens of thousands of items), it consumes significant memory.

iter_find() yields one item at a time and fetches the next page only when the current page is exhausted. Use it whenever the result set might be large or when you want to process items as they arrive.

find() vs iter_find() for large collections
# All sessions in memory — fine for small projects
sessions = project.sessions.find("label=baseline")
print(f"Found {len(sessions)} sessions")

# Lazy iteration — works at any scale
for session in project.sessions.iter_find("label=baseline"):
    process(session)

Practical Patterns

Get or create a container

Get-or-create a project or subject
def get_or_create_project(fw, group, label):
    existing = fw.projects.find_first(f"label={label},group={group}")
    if existing:
        return existing
    new_id = fw.add_project({"label": label, "group": group})
    return fw.get_project(new_id)

def get_or_create_subject(project, label):
    existing = project.subjects.find_first(f"label={label}")
    if existing:
        return existing
    return project.add_subject({"label": label})

Filter on info fields

Custom metadata stored in container .info is queryable using dot notation.

Filter on a custom info field
# Sessions where a custom info field is set to a specific value
sessions = project.sessions.find("info.protocol_version=2")

Null checks

Use null to find containers with missing or unset fields.

Find containers with missing fields
# Subjects with no sex recorded
subjects = project.subjects.find("sex=null")

# Sessions where analysis parent is absent
files = fw.files.find("parents.analysis=null,parent_ref.type=analysis")

Combining date ranges

Multiple conditions on the same field are legal and combine with AND.

Filter sessions within a date range
# Sessions created in a specific window
sessions = project.sessions.find("created>=2023-01-01,created<2024-01-01")

Large collection export

Export all acquisitions from a project
exported = []
for acquisition in project.acquisitions.iter_find(limit=500):
    exported.append({
        "id": acquisition.id,
        "label": acquisition.label,
        "session": acquisition.session,
    })