These guides are primarily for agent readers. They explain how agents use Sails. If you are a person, start at sails.app or Add Sails.

Filter

filter matches, orders, paginates, counts, and sums JSON and YAML front-matter files (.json, .md, .page.md, .page.html). Queries run against a Postgres jsonb index after a file is checkpointed, so they do not re-read object storage for every call.

This is not sql --db. App tables stay on SQL. Page glob: collections stay compile-time page lists. Page filter: collections query the same JSON / front-matter set at request time — see Collections.

Done looks like

mkdir -p sites/demo-filter/posts
cat > sites/demo-filter/posts/a.json <<'EOF'
{"title":"Invoice follow-up","status":"open","amount":120,"date":"2026-09-01"}
EOF
cat > sites/demo-filter/posts/b.json <<'EOF'
{"title":"Closed deal","status":"closed","amount":40,"date":"2026-08-15"}
EOF
filter --where 'status = open' --order date:desc sites/demo-filter/posts

PATH must be under a repo mount (/{org}/{repo}/…). Host /tmp is rejected. Output is a typed JSON array. Each record is the stored object plus _path. With --page / --limit, output is { "items": […], "page": N, "limit": N, "total": N, "pages": N } instead of a bare array.

Worked example

# where + order
filter --where 'status = open' --order date:desc sites/demo-filter/posts

# semantic search (same embedding index as search); default order is relevance
filter --search 'billing invoice' sites/demo-filter/posts

# combine search + where
filter --search 'billing' --where 'status = open' --order date sites/demo-filter/posts

# pagination
filter --where 'status = open' --page 1 --limit 10 sites/demo-filter/posts

# aggregates (no record list)
filter --agg count sites/demo-filter/posts/**/*.json
filter --where 'amount >= 100' --agg count --agg sum:amount sites/demo-filter/posts

# paths only
filter --where 'status = open' --paths sites/demo-filter/posts

# stdin paths from find
find sites/demo-filter/posts -name '*.json' | filter --where 'status = open' --order date

# typed JSON array on stdin (in memory; no index, no --search)
printf '%s\n' '[{"status":"open","amount":120},{"status":"closed","amount":40}]' \
  | filter --where 'status = open'

Capture shapes agents actually use:

rows=$(filter --where 'status = open' sites/demo-filter/posts)   # typed JSON array
page=$(filter --where 'status = open' --limit 10 sites/demo-filter/posts)   # envelope

CLI

filter [--where EXPR] [--search QUERY] [--order FIELD[:asc|:desc]|relevance] \
  [--page N] [--limit N] [--agg count|sum:FIELD]... [--paths] [PATH...]
FlagEffect
--whereSame language as page collections (status = active and amount >= 100). Missing fields are null. Top-level fields only
--searchSemantic query over file contents. Default --order relevance (desc). Records include _score. Ranks up to 100 matches, then applies --where / --order / pagination
--orderWith or without --where. date asc; date:desc or -date desc. relevance / _score require --search
--page / --limitPaginate (1-based page; limit 1–100). --page without --limit uses 10. Output becomes the envelope. --paths still emits a sliced newline list
--aggcount and/or sum:FIELD. No record list. Sum skips null/missing; non-numeric values error. Cannot combine with --page / --limit
--pathsNewline _path values instead of JSON records
PATHFiles, directories (recursive json/md), or globs. No PATH → current directory. Stdin newline paths (from find) are included

Typed JSON arrays on stdin are filtered in memory (no index, no --search).

What gets indexed

Checkpoint indexes .json files and YAML --- front matter (.md, .markdown, .page.md, .page.html, .smd). The 256 KiB cap applies to the JSON file or the YAML block — a long markdown body does not drop a small front-matter document from filter.

Unreadable files (Unix mode) are omitted. sudo filter … (repo owners) bypasses those bits, same as sudo search.

On a static site page

Use front-matter filter: (not glob:) for the same file set:

---
title: Invoices
invoices:
  filter: data/invoices/**
  where: status = open
  sort: date
  order: desc
  paginate: 10
---

A collection must set glob: or filter:, not both. See Collections.

vs SQL vs glob collections

JobTool
App rows (contacts, todos, subscribers)sql --db — SQL
Other pages as a MiniJinja listglob: collection
JSON / front-matter files as a list or CLI queryfilter / page filter:

Failure modes

What you seeWhat it meansWhat to do
Empty arrayNo match, or files not checkpointed yetAutosave the JSON / front-matter files. Confirm path/glob from the cwd you meant
search path must be under a repo mountYou pointed at /tmp or another host pathUse /{org}/{repo}/… (e.g. sites/demo-filter/posts)
You expected contactsThose are table rowssql --db, not filter
--search with no hitsEmbedding index has nothing yet, or query is offWrite/checkpoint files first. Try --where alone
--agg plus --pageNot allowedDrop pagination when aggregating
Non-numeric sum:FIELDSum needs numbersFix the field or skip those files
Unreadable path omittedUnix mode blocked the fileFix mode, or sudo filter as a repo owner

How to: Filter JSON files. Use Keep app data in a database when the job is rows. Also Collections, Static sites.