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.

Names from inside the generator. This page names types from graphitron’s own classification model. They are accurate today, and worth knowing when a rejection message quotes one back at you, but they are not part of the authoring contract: the classification walk that produces them is being drained, and these names retire with it. What stays stable is the schema you write, the directive reference that describes it, and the diagnostics glossary.

@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.

Federation directives on the field (@shareable, @tag) propagate onto the synthesised Connection, Edge, and PageInfo types, so federation contracts that include the field’s tags also include its pagination types. An author-declared PageInfo is left untouched; a synthesised one carries the union of the tags from every connection-promoted 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 Graphitron Development Principles 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.