20.1 Overview
Scopes are named query filters that narrow list endpoint results to a specific subset of records. They are applied via the ?scope= query parameter and provide a declarative alternative to repeating complex filter combinations on every request.
Every resource has at least one scope. Resources with softDelete: true receive three auto-generated scopes (§15.2). All resources support custom scope declarations.
scopes:
published:
filter: "status = 'published' AND deletedAt IS NULL"
default: true
recent:
filter: "createdAt > datetime('now', '-7 days')"
all:
filter: ""
20.2 Scope Declaration
Scopes are declared in the scopes: block of a resource:
resources:
Article:
scopes:
live:
filter: "status = 'published' AND publishedAt <= datetime('now')"
default: true
description: "Published articles with a past or current publish date"
scheduled:
filter: "status = 'scheduled' AND publishedAt > datetime('now')"
description: "Articles scheduled for future publication"
drafts:
filter: "status = 'draft'"
description: "Work-in-progress articles"
archived:
filter: "status = 'archived'"
description: "Articles removed from publication"
all:
filter: ""
description: "All articles regardless of status"
Declaration Keys
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
filter | string | yes | — | SQL WHERE clause fragment applied to the query. Empty string means no filter. |
default | boolean | no | false | If true, this scope is applied when no ?scope= parameter is provided. |
description | string | no | "" | Human-readable description of what records this scope includes. |
Default Scope
At most one scope MAY be marked default: true. When a list request has no ?scope= parameter, the default scope’s filter is applied automatically.
If no scope is marked as default:
- Resources with
softDelete: true→ the auto-generatedactivescope is the default - Resources without soft delete and without a declared default → no filter is applied (all records returned)
Filter Syntax
The filter value is a raw SQL WHERE clause fragment. It is appended to the query’s WHERE clause with AND:
-- Request: GET /articles?scope=live&filter[authorId]=user-1
-- Generated SQL:
SELECT * FROM cms_articles
WHERE (status = 'published' AND publishedAt <= datetime('now')) -- scope
AND authorId = 'user-1' -- filter
ORDER BY createdAt ASC
LIMIT 100
The scope filter and request filters are combined with AND — the scope narrows the base set, then filters narrow further.
Empty Filter
A scope with filter: "" (empty string) applies no filter — all records are returned regardless of status, deletion state, or any other field value:
all:
filter: ""
description: "All records without filtering"
This is useful as an explicit “show everything” option, particularly alongside scopes that filter by default.
20.3 Auto-Generated Scopes
When softDelete: true is set on a resource, three scopes are auto-generated:
# Auto-generated — do not declare these manually
scopes:
active:
filter: "deletedAt IS NULL"
default: true
deleted:
filter: "deletedAt IS NOT NULL"
all:
filter: ""
| Scope | Filter | Description |
|---|---|---|
active | deletedAt IS NULL | Non-deleted records only (default) |
deleted | deletedAt IS NOT NULL | Soft-deleted records only |
all | (no filter) | All records regardless of deletion status |
Override Auto-Generated Scopes
Custom scopes with the same names (active, deleted, all) override the auto-generated ones. This allows customization of the default soft delete behavior:
features:
softDelete: true
scopes:
active:
filter: "deletedAt IS NULL AND status != 'archived'"
default: true
description: "Non-deleted, non-archived records"
This replaces the auto-generated active scope with a stricter filter that also excludes archived records.
20.4 Scope Usage
Basic Usage
GET /api/v1/blog/posts?scope=drafts
Returns only records matching the drafts scope filter.
Scope + Filters
Scopes and request filters combine with AND:
GET /api/v1/blog/posts?scope=live&filter[authorId]=user-1&sort=-publishedAt
The scope filter runs first (published articles with past publish dates), then the authorId filter narrows to one author, then results are sorted by publish date descending.
Scope + Search
GET /api/v1/blog/posts?scope=live&search=kubernetes
Search operates within the scoped result set — only published, live articles are searched.
Scope + Count
GET /api/v1/blog/posts/count?scope=drafts
Returns the count of records matching the scope filter.
Scope + Export
GET /api/v1/blog/posts/export?scope=live
Exports only the records matching the scope filter.
Invalid Scope
If a ?scope= parameter references a scope name that does not exist, the parameter is silently ignored and the default scope (or no scope) is applied. No error is returned.
20.5 Scope Design Patterns
Status-Based Scopes
The most common pattern — one scope per status value:
scopes:
draft:
filter: "status = 'draft'"
pending:
filter: "status = 'pending'"
active:
filter: "status = 'active'"
default: true
archived:
filter: "status = 'archived'"
all:
filter: ""
Time-Based Scopes
Filter by temporal relevance:
scopes:
upcoming:
filter: "startDate > datetime('now')"
default: true
description: "Future events"
past:
filter: "endDate < datetime('now')"
description: "Completed events"
ongoing:
filter: "startDate <= datetime('now') AND endDate >= datetime('now')"
description: "Currently active events"
all:
filter: ""
Ownership Scopes
Filter by the current user’s records. These use a placeholder that the runtime substitutes:
scopes:
mine:
filter: "createdById = :currentUserId"
description: "Records created by the current user"
team:
filter: "departmentId = :currentUserDepartmentId"
description: "Records from the current user's department"
all:
filter: ""
description: "All records (admin view)"
The :currentUserId and :currentUserDepartmentId placeholders are resolved at query time from the authenticated user’s identity. This scope pattern combines with operation-level permissions — a user must have list permission AND the scope filter determines which records they see.
Compound Scopes
Combine multiple conditions for business-meaningful subsets:
scopes:
actionRequired:
filter: "status = 'pending' AND assigneeId IS NOT NULL AND dueDate <= datetime('now', '+3 days')"
description: "Assigned items due within 3 days"
overdue:
filter: "status NOT IN ('completed', 'cancelled') AND dueDate < datetime('now')"
description: "Past-due items that are not completed or cancelled"
unassigned:
filter: "assigneeId IS NULL AND status = 'pending'"
description: "Pending items with no assignee"
20.6 Store-Specific Filter Syntax
Scope filters are SQL fragments and are therefore store-dependent. Date/time functions differ between SQLite and PostgreSQL:
| Operation | SQLite | PostgreSQL |
|---|---|---|
| Current timestamp | datetime('now') | NOW() |
| Date arithmetic | datetime('now', '-7 days') | NOW() - INTERVAL '7 days' |
| Date comparison | date(field) = date('now') | field::date = CURRENT_DATE |
For cross-store compatibility, scope filters SHOULD use ANSI SQL where possible and avoid store-specific functions. When store-specific syntax is unavoidable, the filter value can use the ${STORE} variable to branch:
scopes:
recent:
filter: "${STORE:sqlite}createdAt > datetime('now', '-7 days')${STORE:postgres}createdAt > NOW() - INTERVAL '7 days'"
However, this syntax is complex and rarely needed. Most scope filters use simple comparisons (=, !=, IN, IS NULL) that work identically on both stores. The recommendation is to write filters for the primary store and accept that switching stores may require scope filter updates.
20.7 Scopes and Permissions
Scopes do not bypass RBAC. A scope narrows the query result set, but the user must still have list permission to execute the list query at all. Scopes and permissions are independent mechanisms:
| Mechanism | Controls | Applied At |
|---|---|---|
| Permissions | Who can access the endpoint | Middleware (§4.2.5) |
| Scopes | Which records are returned | Query builder |
A user with list: [authenticated] permission and a ?scope=all parameter sees all records matching the scope — but only if they have list permission. A user without list permission receives 403 regardless of scope.
Scope Visibility
All declared scopes are visible to all users via the schema endpoint. There is no per-scope permission control — any user with list permission can request any scope. If certain record subsets should be hidden from certain users, use field-level permissions (§11.3) or the owner role pattern (§11.4) instead of scopes.
20.8 Schema Endpoint
The schema introspection endpoint returns scope definitions:
{
"scopes": {
"live": {
"filter": "status = 'published' AND publishedAt <= datetime('now')",
"default": true,
"description": "Published articles with a past or current publish date"
},
"scheduled": {
"filter": "status = 'scheduled' AND publishedAt > datetime('now')",
"default": false,
"description": "Articles scheduled for future publication"
},
"drafts": {
"filter": "status = 'draft'",
"default": false,
"description": "Work-in-progress articles"
},
"all": {
"filter": "",
"default": false,
"description": "All articles regardless of status"
}
}
}
A UI generator uses this data to render scope tabs or a scope dropdown above the data table. The default flag identifies which tab is pre-selected. The description provides tooltip or help text.
End of Chapter 20.
End of Volume II — The ADL Domain Language.
Next: Volume III — The Intelligence Layer
Chapter 21: Declarative History of Change