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 null or 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

CollectionDefault limitMaximumFilters and searchContinuation
Intake submissions2550status or state; the two are mutually exclusivenextCursor
People2550q (2–100 characters), statusnextCursor
Organizations2550q (2–100 characters), statusnextCursor
Opportunities2550q, stage, and owner reference or nonenextCursor
Tasks2550q, status, owner; owner defaults to minenextCursor
Cross-resource search2050required q (2–100 characters)no cursor declared
Person timeline50200person path referenceopaque 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 none means 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.