# Pagination

> Relay cursors with a scalar and a PageInfo type per connection, the direction flip on message history, where first is required, and the connections that accept pagination arguments and ignore them.

Page: https://sdk.chatfuel.com/docs/reference/chatfuel-api/pagination
Markdown: https://sdk.chatfuel.com/docs/reference/chatfuel-api/pagination.md

Connections are Relay-shaped, but almost nothing about them is shared — there are fifteen cursor
scalars and fourteen `PageInfo` types, one set per connection.

```graphql
type MessagePage {
  edges: [MessageEdge!]!
  pageInfo: MessagePageInfo!
}

type MessageEdge {
  node: Message!
  cursor: MessagesCursor!
}

type MessagePageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: MessagesCursor
  endCursor: MessagesCursor
}
```

The scalars are `MessagesCursor`, `ContactSearchCursor`, `BotsCursor`, `BotAttributeCursor`,
`WhatsAppTemplateCursor`, `KeywordRuleCursor`, `CommentReplyRulesCursor`,
`CommentReplyRuleV2Cursor`, `FuelyBroadcastCursor`, `MetaAdCursor`, `InstagramMediasCursor`,
`CoworkerConversationsCursor`, `CoworkerMessagesCursor`, `FbPageCursor` and `FbPagePostCursor`.
All of them are opaque strings: echo the values you get back from `pageInfo` and `edges`, and never
build one. One `PageInfo` type is called exactly that, which reads like the shared one and is not —
it belongs to `BotsConnectionV2`, and its cursors are `BotsCursor`.

## Where `first` is required [#where-first-is-required]

```graphql
messages(first: Int, after: MessagesCursor, before: MessagesCursor): MessagePage!
contactsConnection(platforms: [Platform!]!, first: Int!, before: ContactSearchCursor, after: ContactSearchCursor, segment: SegmentInput, orderBy: ContactSearchOrderByInput): ContactConnection!
contactDealsConnection(first: Int!, before: ContactSearchCursor, after: ContactSearchCursor, assigneeFilter: ContactAssigneeFilter!, salesStageV2Filter: SalesStageV2!): ContactConnection!
contactChatsConnection(first: Int!, before: ContactSearchCursor, after: ContactSearchCursor, assigneeFilter: ContactAssigneeFilter!, unreadOnly: Boolean!, salesStageV2Filter: [SalesStageV2!]!, textInputFilter: String): ContactConnection!
```

`first` is optional on `Conversation.messages` and required on the three contact-ish connections.
It varies per field, so read the signature in the bundled schema instead of assuming.

## Message history runs backwards [#message-history-runs-backwards]

| Arguments             | Order you get                      | What paging forward means                                                               |
| --------------------- | ---------------------------------- | --------------------------------------------------------------------------------------- |
| No cursor, or `after` | Descending — newest message first. | `after: pageInfo.endCursor` walks **backwards into history**, one older page at a time. |
| `before`              | Ascending.                         | Forward in time.                                                                        |

A chat UI loads `messages(first: 100)`, then answers "scroll up for older" with
`messages(first: 100, after: <endCursor>)`. `pageInfo.startCursor` is the newest message's cursor,
which is what a mark-as-read call wants.

## Connections that take pagination arguments and ignore them [#connections-that-take-pagination-arguments-and-ignore-them]

| Field                                                                                                                                                  | What it actually does                                                                                                                              |
| ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Bot.whatsAppTemplates(first, after, before)`                                                                                                          | Accepts all three and ignores them: it returns the full list every time. The field is also nullable.                                               |
| `Bot.goodsCatalog`, `Bot.specialists`, `Bot.contactScopes`, `Bot.members`, `Bot.invites`, `Bot.bookingsV2`, `Workspace.bots`, `currentUser.workspaces` | No pagination arguments at all: each returns its complete result every time. `bookingsV2` takes a start and an end time, but nothing to page with. |

## Accumulating pages alongside live updates [#accumulating-pages-alongside-live-updates]

The chat, contact and deal lists are forward-only in practice: page with `after:
pageInfo.endCursor`, guard on `hasNextPage`, and never start a page fetch while the previous one is
still in flight.

| Rule                                                      | Why                                                                                                                                                                                                           |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| De-duplicate by node id, replacing rather than appending. | An incoming page or update can contain nodes you already hold.                                                                                                                                                |
| Re-sort yourself.                                         | `contactsChatUpdates` and `contactsDealUpdates` tell you what changed, not where it belongs — sort the chat list by `lastConversationMessageTime` descending, deals by `lastSalesStageUpdateTime` descending. |
| Merge messages by `clientId`, never by `id`.              | `Message.id` is nullable; `clientId` is the reliable key.                                                                                                                                                     |
| Treat an invalid-cursor error as "start again".           | An `after` cursor that has fallen out of the result window stops resolving; refetch from the first page.                                                                                                      |

<Callout type="warn">
  Changing any filter argument resets the connection. The cursors you hold belong to the old
  argument set, so the accumulated edges have to be thrown away — keep them and the list quietly
  becomes two different result sets stacked on top of each other.
</Callout>
