Pagination, filtering, and search
Collection behavior is App-specific. Preserve cursors exactly, repeat the query that produced them, and do not construct or decode cursor values.
Cursor model
Vision Apps use opaque cursor pagination where declared. A cursor represents continuation state chosen by the owning App; it is not a public record ID, page number, database offset, or timestamp contract.
Rules for every cursor-based traversal:
- Use only a cursor returned by the same operation.
- Do not edit, decode, concatenate, log, or expose cursors in browser analytics.
- Preserve the filter, search, time-window, ownership, and limit parameters that produced the cursor unless the operation explicitly permits a change.
- Treat an invalid or expired cursor as a new traversal, not as permission to guess continuation state.
- Stop when the response cursor is
nullor the response's completion flag says no more records. - Deduplicate by stable public reference if your workflow can restart after failure.
- Do not run pages concurrently unless stable partitioning and ordering are documented.
CRM collections
| Collection | Default limit | Maximum | Filters and search | Continuation |
|---|---|---|---|---|
| Intake submissions | 25 | 50 | status or state; the two are mutually exclusive | nextCursor |
| People | 25 | 50 | q (2–100 characters), status | nextCursor |
| Organizations | 25 | 50 | q (2–100 characters), status | nextCursor |
| Opportunities | 25 | 50 | q, stage, and owner reference or none | nextCursor |
| Tasks | 25 | 50 | q, status, owner; owner defaults to mine | nextCursor |
| Cross-resource search | 20 | 50 | required q (2–100 characters) | no cursor declared |
| Person timeline | 50 | 200 | person path reference | opaque nextCursor |
CRM read-model cursors are signed and resource-bound in implementation evidence. Presenting a cursor from one collection to another fails validation rather than exposing another collection's continuation state. The public contract does not yet state cursor lifetime or a universal ordering guarantee.
The CRM funnel is a bounded analytical window rather than a paginated list. from defaults to 30 days before to; to defaults to the current time. Both are RFC 3339 date-time values when supplied.
Calendar collections
The Calendar agenda requires from and to, defaults to 50 records, permits 1–100, and exposes an optional cursor of at most 500 characters.
Availability uses an explicit time window and returns availability without disclosing event details. Open-slot calculation is a command-style operation over bounded actors and time ranges; use its request schema rather than treating it as general free-text search.
Calendar cursor lifetime, ordering under concurrent event changes, and snapshot isolation are not yet public commitments.
Search and filters
- Search text is trimmed and contract-bounded.
- Search is scoped to records authorized for the current workspace and actor context.
- An empty result does not prove that no matching private record exists.
- Enum filters are exact contract values; do not send UI labels.
- Owner filters use public actor references where declared. CRM's opportunity owner value
nonemeans the unowned work pool. - Date-time boundaries require explicit offsets.
- Unknown query parameters may be rejected by strict operation validation.
- No estate-wide sort parameter is currently declared. Do not depend on incidental database order.
Safe traversal
Sequential cursor traversal
const seen = new Set<string>()
let cursor: string | null = null
do {
const url = new URL('/api/crm/v1/people', CRM_BASE_URL)
url.searchParams.set('limit', '50')
url.searchParams.set('status', 'active')
if (cursor) url.searchParams.set('cursor', cursor)
const response = await fetch(url, {
headers: {
authorization: 'Bearer ' + accessToken,
accept: 'application/json',
},
signal: AbortSignal.timeout(30_000),
})
if (!response.ok) throw new Error(`Vision CRM ${response.status}`)
const page = await response.json()
for (const person of page.items) {
if (seen.has(person.publicReference)) continue
seen.add(person.publicReference)
await processPerson(person)
}
cursor = page.nextCursor
} while (cursor)
Checkpoint the last accepted cursor only with the complete query definition and contract version. If the App rejects the cursor after a long pause, restart and deduplicate by public reference.
Known gaps
The following are not yet uniform public guarantees:
- cursor lifetime and revocation;
- whether cursors are bound to every query parameter;
- snapshot versus live traversal consistency;
- stable ordering and tie-break fields;
- behavior when records are created, updated, or removed between pages;
- maximum total traversal;
- case, accent, tokenization, and ranking behavior for search;
- null sorting and locale behavior.
Operation pages expose what the approved contract does declare. Applications that require stronger guarantees should pause until the owning App publishes them.