Skip to content

Spec edits for incremental delivery, Section 3 only #1132

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: incremental-integration
Choose a base branch
from
127 changes: 125 additions & 2 deletions spec/Section 3 -- Type System.md
Original file line number Diff line number Diff line change
Expand Up @@ -816,8 +816,8 @@ And will yield the subset of each object type queried:
When querying an Object, the resulting mapping of fields are conceptually
ordered in the same order in which they were encountered during execution,
excluding fragments for which the type does not apply and fields or fragments
that are skipped via `@skip` or `@include` directives. This ordering is
correctly produced when using the {CollectFields()} algorithm.
that are skipped via `@skip` or `@include` directives or postponed via `@defer`.
This ordering is correctly produced when using the {CollectFields()} algorithm.

Response serialization formats capable of representing ordered maps should
maintain this ordering. Serialization formats which can only represent unordered
Expand Down Expand Up @@ -1973,6 +1973,15 @@ GraphQL implementations that support the type system definition language must
provide the `@deprecated` directive if representing deprecated portions of the
schema.

GraphQL implementations may provide the `@defer` and/or `@stream` directives. If
either or both of these directives are provided, they must conform to the
requirements defined in this specification.

Note: The [Directives Are Defined](#sec-Directives-Are-Defined) validation rule
ensures that GraphQL operations can only include directives available on the
schema; thus operations including `@defer` or `@stream` directives can only be
executed by a GraphQL service that supports them.

GraphQL implementations that support the type system definition language should
provide the `@specifiedBy` directive if representing custom scalar definitions.

Expand Down Expand Up @@ -2190,3 +2199,117 @@ to the relevant IETF specification.
```graphql example
scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122")
```

### @defer

```graphql
directive @defer(
if: Boolean! = true
label: String
) on FRAGMENT_SPREAD | INLINE_FRAGMENT
```

The `@defer` directive may be provided on a fragment spread or inline fragment
to indicate that execution of the related selection set should be deferred. When
a request includes the `@defer` directive, it may return an _incremental stream_
consisting of an _initial execution result_ containing all non-deferred data,
followed by one or more _subsequent execution result_ including the deferred
Comment on lines +2215 to +2216
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Terminology:

  • initial execution result - confirms to execution result (just adds a couple more fields) 👍
  • subsequent execution result - sounds like it should conform to execution result, but doesn't ({hasNext: false} very obviously fails this) 👎

If we continue with this naming, we should revisit the definition of execution result.

Did we already discuss changing to subsequent incremental execution result? To my ear the word incremental1 makes it much clearer that the payload won't necessarily conform to execution result (a bit like "partial" - suddenly all the fields are no longer required).

Footnotes

  1. You could think of each additional payload in a subscription as being a "subsequent execution result", but the term "incremental execution result" would not fit for subscriptions - it's clearly a different beast.

Copy link
Contributor Author

@robrichard robrichard Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjie I decided to go with subsequent execution result instead of incremental execution result because we also need a name for the object in the incremental entry. As of the last discussion we are calling it an incremental result, (along with the other objects completed result, pending result).

The argument about subscriptions is compelling so I'm open to changing it, but I'm concerned having both an Incremental Execution Result and an Incremental Result will be confusing.

data.

The `@include` and `@skip` directives take precedence over `@defer`.

```graphql example
query myQuery($shouldDefer: Boolean! = true) {
user {
name
...someFragment @defer(label: "someLabel", if: $shouldDefer)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
...someFragment @defer(label: "someLabel", if: $shouldDefer)
...someFragment @defer(if: $shouldDefer, label: "someLabel")

}
}
fragment someFragment on User {
id
profile_picture {
uri
}
}
```

#### @defer Arguments

- `if: Boolean! = true` - When `true`, fragment _should_ be deferred (see
related note below). When `false`, fragment must not be deferred. Defaults to
Comment on lines +2238 to +2239
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, but we would expect this note to be in the same section. Having it under @stream's heading is unexpected without further signposting.

`true` when omitted.
- `label: String` - An optional string literal (variables are disallowed) used
by GraphQL clients to identify data in the _incremental stream_ and associate
it with the corresponding defer directive. If provided, the GraphQL service
must include this label in the corresponding pending object within the
_incremental stream_. The `label` argument must be unique across all `@defer`
and `@stream` directives in the document.

### @stream

```graphql
directive @stream(
if: Boolean! = true
label: String
initialCount: Int! = 0
) on FIELD
```

The `@stream` directive may be provided for a field whose type incorporates a
`List` type modifier. The directive enables returning a partial list initially,
followed by additional items in one or more _subsequent execution result_. If
the field type incorporates multiple `List` type modifiers, only the outermost
list is streamed.

Note: The mechanism through which items are streamed is implementation-defined
and may use technologies such as asynchronous iterators.

The `@include` and `@skip` directives take precedence over `@stream`.

```graphql example
query myQuery($shouldStream: Boolean! = true) {
user {
friends(first: 10)
@stream(label: "friendsStream", initialCount: 5, if: $shouldStream) {
Copy link
Member

@benjie benjie Jul 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@stream(label: "friendsStream", initialCount: 5, if: $shouldStream) {
@stream(if: $shouldStream, label: "friendsStream", initialCount: 5) {

name
}
}
}
```

#### @stream Arguments

- `if: Boolean! = true` - When `true`, field _should_ be streamed (see related
note below). When `false`, the field must behave as if the `@stream` directive
is not present—it must not be streamed and all of the list items must be
included. Defaults to `true` when omitted.
- `label: String` - An optional string literal (variables are disallowed) used
by GraphQL clients to identify data in the _incremental stream_ and associate
it with the corresponding stream directive. If provided, the GraphQL service
must include this label in the corresponding pending object within the
_incremental stream_. The `label` argument must be unique across all `@defer`
and `@stream` directives in the document.
- `initialCount: Int! = 0` - The number of list items to include initially when
completing the parent selection set. If omitted, defaults to `0`. An execution
error will be raised if the value of this argument is less than `0`. When the
size of the list is greater than or equal to the value of `initialCount`, the
GraphQL service _must_ initially include at least as many list items as the
value of `initialCount` (see related note below).

Note: The
[Defer And Stream Directive Labels Are Unique](#sec-Defer-And-Stream-Directive-Labels-Are-Unique)
validation rule ensures uniqueness of the values passed to `label` on both the
`@defer` and `@stream` directives. Variables are disallowed in the `label`
because their values may not be known during validation.

Note: The ability to defer and/or stream data can have a potentially significant
impact on application performance. Developers generally need clear, predictable
control over their application's performance. It is highly recommended that
GraphQL services honor the `@defer` and `@stream` directives on each execution.
However, the specification allows advanced use cases where the service can
determine that it is more performant to not defer and/or stream. Therefore,
GraphQL clients _must_ be able to process a _response_ that ignores individual
`@defer` and/or `@stream` directives. This also applies to the `initialCount`
argument on the `@stream` directive. Clients must be able to process a streamed
field result that contains more initial list items than what was specified in
the `initialCount` argument.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like it warrants its own header, similar to Supporting Subscriptions at Scale (beautiful bit of alliteration there!), that can be referenced by both blocks.

Also, this feels like it should be normative. Putting "must" in a non-normative note feels wrong.

We should also be much clearer that this is on a case by case basis, so for example:

{
  list @stream {
    field1
    ...@defer {
      field2
    }
  }
}

In this case, different entries in the streamed list may or may not defer field2 - the @defer is not ignored "wholesale" but on an execution-position by execution-position basis.

Also, I think we should more strongly empower schema authors to opt out when they see fit, because there are many situations where streaming may add complexity without reducing latency for the client. For example: reverse cursor pagination...

query Things($cursor: String) {
  things(first: 1000, after: $cursor) {
    nodes @stream(initialCount: 2) { id name }
  }
}

^ for this, it can clearly stream from the datasource; however:

query ThingsReverse($cursor: String) {
  things(last: 1000, before: $cursor) {
    nodes @stream(initialCount: 2) { id name }
  }
}

^ in this case, you can't necessarily stream from the datasource, because the last record to come from the datasource (assuming you're fetching in reverse, discovering each next row as you go) is actually the first record you should return to the user.

Similarly:

query ThingsAwkward($cursor: String) {
  things(first: 1000, last: 100, after: $cursor) {
    nodes @stream(initialCount: 2) { id name }
  }
}

^ in this case we may not know which the last 100 will be until we've fetched them all. (This is of course a pathological query.)

Of course streaming the underlying data from your data source and streaming the result of the field's execution are two different things (and happen at different positions in the waterfall), so YMMV.