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.

Collections

A collection is a named list of other pages on the same site. The compiler fills it when the page is first requested. It is not Postgres, not a JSON store, and not something you INSERT into.

For app records (contacts, tasks, subscribers), use sql --db. See Keep app data in a database.

Done looks like

An index page lists every matching .page.md / .page.html. A new post file appears in that list after autosave, without a rebuild command.

Worked example: a blog index

This site already does it. pages/index.page.html and pages/how-to/index.page.html both declare a glob.

---
title: How to
howtos:
  glob: pages/how-to/*.page.md
  where: draft != true
  sort: nav_order
  order: asc
---
{% for page in howtos %}
  <a href="/guides/{{ page._url }}">{{ page.title }}</a>
{% endfor %}

From a fresh site:

cp -r /templates/static-blog ./site

site/pages/index.page.html already has posts: with glob: pages/blog/**. Add site/pages/blog/launch.page.md:

---
title: Launch
date: 2026-08-19
draft: false
---
<site-shell title="{{ title }}">
# Launch

Shipped.
</site-shell>

Publish once (publish create site --path site --policy 644). Open /. The homepage loop over posts includes Launch. Edit the file; autosave is enough.

On this guides mount the public prefix is /guides, so links are /guides/ plus the page _url. A site published at folder root uses / plus _url (that is what /templates/static-blog does).

Front-matter keys

Declare the list under any name (posts, howtos, features). Required key is glob.

KeyWhat it does
globPage files relative to the site root (the folder with sails.site.json). Example: pages/blog/**
whereSQL-like filter on front matter. Missing fields are null. There is no filter: key
sortFront-matter field name (date, nav_order, title)
orderasc or desc

where examples that work:

where: draft != true
where: draft != true and date != null
where: id = $threadId

$threadId is a splat from a file like pages/threads/$threadId.page.html. The captured value is also a template variable named threadId.

Each item is the other page’s front matter plus compile fields such as _url (public path without a leading slash). Read title, date, and _url on each item.

What is not a collection

Tempting file / keyReality
sails.data.jsonDo not use. Not how app records work
Page from: or page sql:Do not use for records
collection query contactsNot a workspace command for this. Query Postgres with sql --db
Writing a row from a formHandler talks to sql --db, not to a glob

Copy /templates/crm-app (or remix-app / table-app) when the job is rows.

Failure modes

What you seeWhat it meansWhat to do
Loop is emptyGlob missed, or where dropped every pagePath is from the site root (pages/blog/**, not blog/**). draft: true pages are excluded by draft != true. A page with no draft key is null, so draft != true still includes it
New post missing on the live URLCache still has the old collectionAutosave the new file. The page recompiles when collection files change. Do not publish create again
filter: in front matterThat key is not a collection filterUse where:
You put contacts in sails.data.jsonWrong store.db bundle + sql --db. See Keep app data in a database
check --path template errorThe template failed the same way publish wouldFix the loop syntax and quotes. Do not HTML-escape quotes inside if expressions

How to: Publish a site, Keep app data in a database. Also Static sites.