---
name: graphql-sdl-conventions
source: https://app.decimal.ai/s/graphql-sdl-conventions@1/SKILL.md
source_sha256: 8ca0ce7ff97e
---

# GraphQL SDL conventions

## Contract

When you write or edit a GraphQL schema in the Schema Definition Language (SDL),
conform to the spec's type system and the community naming conventions: PascalCase type
names, camelCase fields/arguments, SCREAMING_SNAKE_CASE enum values, the exactly-five
built-in scalars (`Int` `Float` `String` `Boolean` `ID`) with everything else declared as
a custom `scalar`, non-null `!` discipline, `Input`/`Payload` mutation suffixes, and the
built-in `@deprecated(reason:)` directive for retiring fields. Apply this to any object
type, interface, union, enum, input, mutation, or scalar you emit.

## Rules

1. **Type names are PascalCase.** Every *named type* — `type`, `interface`, `union`,
   `enum`, `input`, and any custom `scalar` — is UpperCamelCase: `PaymentMethod`,
   `ShippingAddress`. Never snake_case (`payment_method`) or camelCase (`paymentMethod`)
   for a type name. The three root operation types are named exactly `Query`, `Mutation`,
   `Subscription`.

2. **Field and argument names are camelCase.** Every field and every argument is
   lowerCamelCase: `phoneNumber`, `createdAt`, argument `orderBy`. Convert any snake_case
   the request hands you (`phone_number`, `created_at`) to camelCase — never keep
   snake_case, SCREAMING_SNAKE_CASE, or PascalCase on a field.

3. **Enum values are SCREAMING_SNAKE_CASE.** Every enum member is all-uppercase, words
   joined by underscores: `PENDING`, `AWAITING_PAYMENT`. Never lowercase (`pending`),
   camelCase (`awaitingPayment`), or PascalCase (`AwaitingPayment`) enum members. The enum
   *type* itself is still PascalCase.

4. **Use only the five built-in scalars.** The complete built-in scalar set is `Int`
   (32-bit signed whole number), `Float` (double-precision decimal), `String`, `Boolean`,
   and `ID`. Use `Int` for counts/quantities — never `Integer`, `Number`, or `Long`. Use
   `Float` for decimals — never `Double` or `Decimal`. Use `Boolean` — never `Bool`. Any
   other primitive (a date, timestamp, URL, email, JSON blob, decimal money) is **not**
   built in: declare it once with the `scalar` keyword (`scalar DateTime`) and reference
   that, or fall back to `String`. Never write `Date`, `DateTime`, `Timestamp`, `JSON`, or
   `URL` as a field type without a matching `scalar` declaration.

5. **Mark required fields non-null with `!`.** A field that is always present carries a
   trailing `!` (`String!`). The primary identifier is `id: ID!`. Lists: `[T]` is a
   nullable list of nullable items; a required list of required items is `[T!]!`. Choose
   nullability deliberately — do not make everything nullable and do not make everything
   non-null.

6. **Group mutation arguments into an `Input` type.** When a mutation takes more than a
   trivial argument, define an Input Object with the `input` keyword (not `type`) and name
   it with the `Input` suffix: `input CreateBookingInput { ... }`. The mutation then takes
   a single argument of that input type. Input fields follow the same camelCase rule.

7. **Return a `Payload` (or `Response`) type from mutations.** A mutation returns a
   dedicated object type whose name ends in `Payload` or `Response` (`CreateBookingPayload`),
   never the bare entity type and never a naked scalar like `Boolean`. Mutation field names
   are camelCase verbs: `createBooking`, `cancelReservation`.

8. **Retire fields with `@deprecated`, don't delete them.** To phase out a field or enum
   value that clients may still use, keep it in the schema and annotate it with the
   built-in `@deprecated(reason: "...")` directive; the `reason` string names the
   replacement. Never silently delete it, comment it out, or rename it in place.

## Worked examples

BEFORE — snake_case fields, wrong scalars, no non-null (the base's reflexive default):

```graphql
type blog_post {
  post_id: Integer
  created_at: Date
  view_count: Number
}
```

AFTER — PascalCase type, camelCase fields, built-in + declared scalars, non-null id:

```graphql
scalar DateTime

type BlogPost {
  postId: ID!
  createdAt: DateTime!
  viewCount: Int!
}
```

BEFORE — enum members in the wrong case:

```graphql
enum Direction {
  north
  East
  southWest
}
```

AFTER — SCREAMING_SNAKE_CASE members, PascalCase enum name:

```graphql
enum Direction {
  NORTH
  EAST
  SOUTH_WEST
}
```

BEFORE — a mutation with loose arguments returning the bare entity:

```graphql
type Mutation {
  addProduct(name: String, price: Double, active: Bool): Product
}
```

AFTER — `input` argument object, `Int`/`Float`/`Boolean` scalars, `Payload` result:

```graphql
input AddProductInput {
  name: String!
  price: Float!
  active: Boolean!
}

type AddProductPayload {
  product: Product!
}

type Mutation {
  addProduct(input: AddProductInput!): AddProductPayload!
}
```

BEFORE — a retired field simply removed / commented out:

```graphql
type Account {
  # username removed, use handle now
  handle: String!
}
```

AFTER — kept and marked with the directive so existing clients keep working:

```graphql
type Account {
  username: String! @deprecated(reason: "Use `handle` instead.")
  handle: String!
}
```

## Edge cases & exceptions

- **Money and precise decimals.** Do not invent `Money`/`Decimal` as a built-in; either
  model as `Int` in the smallest currency unit (cents) or declare a custom `scalar Decimal`.
- **Multi-word enum values.** Join words with an underscore and uppercase every letter:
  "awaiting refund" → `AWAITING_REFUND`, not `AWAITINGREFUND` or `AwaitingRefund`.
- **Acronyms in type names.** Keep PascalCase; prefer `HttpEndpoint`/`ApiKey` style and be
  consistent — the type name is still Upper-first.
- **Trivial single-scalar mutation.** A one-argument mutation (e.g. delete-by-id) may take
  the scalar directly (`id: ID!`) but still returns a `Payload`/`Response` type.
- **Deprecating an enum value.** The `@deprecated` directive works on enum values too:
  `LEGACY @deprecated(reason: "Use ACTIVE.")`.
- **Interfaces and unions.** `type Photo implements Node { ... }` (interfaces joined by
  `&`); `union SearchResult = Photo | Article` — both type names PascalCase.

## Do / Don't

- DO name types PascalCase and fields/arguments camelCase.
- DON'T carry snake_case field names (`user_id`, `created_at`) into the schema.
- DO write enum values in SCREAMING_SNAKE_CASE.
- DON'T use lowercase, camelCase, or PascalCase enum members.
- DO use `Int`, `Float`, `Boolean`, `ID`, `String` and declare anything else as `scalar`.
- DON'T reference `Integer`, `Number`, `Double`, `Bool`, or an undeclared `Date`/`DateTime`.
- DO mark required fields with `!` and give the id `ID!`.
- DO group mutation arguments in an `input` type named `...Input` and return `...Payload`.
- DON'T pass loose scalar arguments and return the bare entity from a mutation.
- DO retire fields with `@deprecated(reason: "...")`.
- DON'T delete, comment out, or silently rename a field clients may still use.

## Common mistakes

- Echoing snake_case field names straight from a database schema or the prompt.
- Writing enum members as `active`/`Active` instead of `ACTIVE`.
- Typing whole numbers as `Integer`/`Number` and decimals as `Double`/`Decimal`.
- Treating `Date`/`DateTime`/`JSON`/`URL` as built-in scalars without a `scalar` declaration.
- Returning `Boolean` or the bare entity from a mutation instead of a `Payload` type.
- Passing many positional scalar arguments to a mutation instead of one `Input` object.
- Deleting a field during a rename instead of `@deprecated`-annotating the old one.

## Quick checklist

- [ ] Type/interface/union/enum/input/scalar names are PascalCase; roots are Query/Mutation/Subscription.
- [ ] Fields and arguments are camelCase (no snake_case).
- [ ] Enum values are SCREAMING_SNAKE_CASE.
- [ ] Only `Int`/`Float`/`String`/`Boolean`/`ID` used as built-ins; everything else is a declared `scalar`.
- [ ] Required fields end in `!`; primary id is `ID!`.
- [ ] Mutation args grouped in an `...Input`; result is a `...Payload`/`...Response` type.
- [ ] Retired fields are kept and marked `@deprecated(reason: "...")`.
