Marks a list field as a Relay cursor connection. The schema-transform stage rewrites the field’s return type from [T!]! to a synthesized TConnection carrying edges, nodes, pageInfo, and totalCount; the generator emits keyset-based seek/limit SQL with ROW_NUMBER() OVER (PARTITION BY …​) for batched per-parent pagination. If the field doesn’t already declare pagination arguments, forward-pagination args (first: Int, after: String) are added automatically; backward (last/before) and dynamic @orderBy arguments coexist when present.

@asConnection works on three field shapes that the rewrite recognises distinctly: bare list fields on @table parents (inline pagination), @reference-driven child lists (per-parent paginated children), and union/interface lists (multi-table polymorphic pagination). It also composes with @splitQuery to dispatch each parent’s connection through a DataLoader.

SDL signature

directive @asConnection(
    defaultFirstValue: Int = 100
    connectionName:    String
) on FIELD_DEFINITION

Parameters

Name Type Default Description

defaultFirstValue

Int

100

Page size used when the client omits first:. Not a cap on first:; clients that supply a value get exactly what they ask for.

connectionName

String

{ParentType}{FieldName}Connection

Deprecated. Override the synthesized connection type’s name. Each connection field should own its own type; sharing a Connection across fields conflates distinct semantic contexts (different parents, filters, orders) at the type level. The override remains supported as a transition mechanism for legacy schemas but is permanently discouraged. See How-to: Cursor-paginated connections.

Canonical examples

A bare list on a @table parent (Query.stores): @asConnection rewrites the return type to StoreConnection, adds first:/after: arguments, and emits keyset pagination using @defaultOrder for the cursor:

type Query {
    stores: [Store!]! @asConnection @defaultOrder(primaryKey: true)
}

A union-typed list (Address.occupantsConnection): the multi-table polymorphic emitter wraps the per-branch UNION ALL in a derived table so seek/limit and cursor pagination apply across the union; totalCount counts only this parent’s occupants:

type Address @table(name: "address") {
    occupants:           [AddressOccupant!]!
    occupantsConnection: [AddressOccupant!]! @asConnection(defaultFirstValue: 5)
}

union AddressOccupant = Customer | Staff

Per-parent paginated children with @splitQuery (Film.actorsConnection): a ROW_NUMBER() OVER (PARTITION BY parentInput.idx) envelope gives each parent its own paginated slice, and a DataLoader batches the dispatch:

type Film @table(name: "film") {
    actorsConnection(
        first: Int, last: Int, after: String, before: String
    ): ActorsConnection! @splitQuery @reference(path: [
        {key: "film_actor_film_id_fkey"},
        {key: "film_actor_actor_id_fkey"}
    ]) @defaultOrder(primaryKey: true)
}

When the schema declares the connection type explicitly (the ActorsConnection! shape above), the rewrite reuses it instead of synthesising a fresh one. Otherwise, a *Connection, *Edge, and PageInfo are generated alongside the field.

Dynamic order via @orderBy composes naturally:

actorsOrderedConnection(
    order: [ActorOrderBy] @orderBy,
    first: Int, last: Int, after: String, before: String
): ActorsConnection! @splitQuery @reference(path: [
    {key: "film_actor_film_id_fkey"},
    {key: "film_actor_actor_id_fkey"}
]) @defaultOrder(primaryKey: true)

The cursor encodes the active order columns (the @orderBy selection if the client supplied one, otherwise @defaultOrder); decoding rejects cursors whose embedded sort doesn’t match the request’s effective order.

Constraints

  • Applies to list-typed fields. The runtime cardinality must be a list; single-cardinality @asConnection is rejected at classify time.

  • The field must have a stable order. Either @defaultOrder is set on the field, or @orderBy is required at runtime; without one of these, keyset pagination has no anchor and the build fails.

  • The synthesized connection type’s name must not collide with another type already declared in the schema. The default <ParentType><FieldName>Connection shape rarely collides because parent and field names jointly disambiguate; collisions usually mean two carriers should be split into distinct fields rather than coalesced. The deprecated connectionName: override exists as a transition escape hatch.

  • defaultFirstValue: is the page size used when the client omits first:; it is not a cap. The rewrite plugin does not ship a server-side maximum on first:/last: (the legacy generator’s <maxAllowedPageSize> setting was excised); enforce caps at the API layer if needed. See How-to: Cursor-paginated connections.

  • Composes with @splitQuery: when both are present, the connection is materialized per-parent through a DataLoader keyed on the parent’s PK or FK. Without @splitQuery on a parent-keyed list, all parents' rows are paginated in a single SQL pass (the PARTITION BY envelope is omitted).

  • Inline TableField placement is not allowed under interface-or-union return types unless the surrounding type axis supports the multi-table emitter; the generator rejects mismatched placements at classify time. (See rewrite-design-principles.adoc for the "no generator-side invariant goes unchecked at validate time" rule that backs this.)

See also

  • @splitQuery dispatches per-parent connections through a DataLoader for nested-depth pagination.

  • @defaultOrder anchors the cursor when the client omits @orderBy.

  • @orderBy supplies dynamic order; the cursor encodes the active selection.

  • How-to: Cursor-paginated connections covers the one-field-one-connection rule, totalCount performance, and what the rewrite does about page-size caps.