Skip to main content
Mutations are used to create, update, and delete data in the Openlane GraphQL API. This guide covers common mutation patterns and real examples from the codebase.

Mutation Structure

Mutations are defined using the gql template tag, similar to queries:
import { gql } from 'graphql-request'

export const UPDATE_USER = gql`
  mutation UpdateUser(
    $updateUserId: ID!
    $input: UpdateUserInput!
    $avatarFile: Upload
  ) {
    updateUser(
      id: $updateUserId
      input: $input
      avatarFile: $avatarFile
    ) {
      user {
        id
        avatarFile {
          presignedURL
        }
      }
    }
  }
`

Common Mutation Patterns

Create Mutation

Create a new resource:
packages/codegen/query/organization.ts
export const CREATE_ORGANIZATION = gql`
  mutation CreateOrganization($input: CreateOrganizationInput!) {
    createOrganization(input: $input) {
      organization {
        id
      }
    }
  }
`
Generated Hook:
const { mutateAsync: createOrg, isPending } = useCreateOrganization()

await createOrg({
  input: {
    name: 'acme-corp',
    displayName: 'Acme Corporation'
  }
})

Update Mutation

Update an existing resource:
packages/codegen/query/user.ts
export const UPDATE_USER = gql`
  mutation UpdateUser(
    $updateUserId: ID!
    $input: UpdateUserInput!
    $avatarFile: Upload
  ) {
    updateUser(
      id: $updateUserId
      input: $input
      avatarFile: $avatarFile
    ) {
      user {
        id
        avatarFile {
          presignedURL
        }
      }
    }
  }
`
Usage:
const { mutateAsync: updateUser, isPending } = useUpdateUser()

await updateUser({
  updateUserId: userId,
  input: {
    firstName: 'John',
    lastName: 'Doe',
    displayName: 'John Doe'
  }
})

Delete Mutation

Delete a resource:
packages/codegen/query/organization.ts
export const DELETE_ORGANIZATION = gql`
  mutation DeleteOrganization($deleteOrganizationId: ID!) {
    deleteOrganization(id: $deleteOrganizationId) {
      deletedID
    }
  }
`
Usage:
const { mutateAsync: deleteOrg } = useDeleteOrganization()

const result = await deleteOrg({
  deleteOrganizationId: orgId
})

console.log('Deleted:', result.data.deleteOrganization.deletedID)

Bulk Create Mutation

Create multiple resources at once:
packages/codegen/query/organization.ts
export const CREATE_BULK_INVITE = gql`
  mutation CreateBulkInvite($input: [CreateInviteInput!]) {
    createBulkInvite(input: $input) {
      invites {
        id
      }
    }
  }
`
Usage:
const { mutateAsync: createInvites } = useCreateBulkInvite()

await createInvites({
  input: [
    { recipient: '[email protected]', role: 'MEMBER' },
    { recipient: '[email protected]', role: 'ADMIN' }
  ]
})

Update Nested Resource

Update a nested resource like settings:
packages/codegen/query/user.ts
export const UPDATE_USER_SETTING = gql`
  mutation UpdateUserSetting(
    $updateUserSettingId: ID!
    $input: UpdateUserSettingInput!
  ) {
    updateUserSetting(
      id: $updateUserSettingId
      input: $input
    ) {
      userSetting {
        id
      }
    }
  }
`
Usage:
const { mutateAsync: updateSettings } = useUpdateUserSetting()

await updateSettings({
  updateUserSettingId: settingId,
  input: {
    status: 'ACTIVE',
    tags: ['premium', 'verified']
  }
})

Transfer Ownership Mutation

Special mutation for transferring resource ownership:
packages/codegen/query/organization.ts
export const TRANSFER_ORGANIZATION_OWNERSHIP = gql`
  mutation TransferOrganizationOwnership($newOwnerEmail: String!) {
    transferOrganizationOwnership(newOwnerEmail: $newOwnerEmail) {
      invitationSent
    }
  }
`
Usage:
const { mutateAsync: transferOwnership } = useTransferOrganizationOwnership()

const result = await transferOwnership({
  newOwnerEmail: '[email protected]'
})

if (result.data.transferOrganizationOwnership.invitationSent) {
  toast.success('Ownership transfer invitation sent')
}

Using Mutations in Components

Real example from the codebase:
apps/console/src/components/pages/protected/profile/user-settings/profile-name-form.tsx
import { useGetCurrentUser, useUpdateUser } from '@/lib/graphql-hooks/user'
import { useNotification } from '@/hooks/useNotification'
import { parseErrorMessage } from '@/utils/graphQlErrorMatcher'

const ProfileNameForm = () => {
  const [isSuccess, setIsSuccess] = useState(false)
  const { isPending: isSubmitting, mutateAsync: updateUserName } = useUpdateUser()
  const { successNotification, errorNotification } = useNotification()

  const { data: sessionData } = useSession()
  const userId = sessionData?.user.userId

  const onSubmit = async (data) => {
    try {
      await updateUserName({
        updateUserId: userId,
        input: {
          firstName: data.firstName,
          lastName: data.lastName,
          displayName: data.displayName,
          email: data.email,
        },
      })
      setIsSuccess(true)
      successNotification({ title: 'Profile updated successfully!' })
    } catch (error) {
      const errorMessage = parseErrorMessage(error)
      errorNotification({
        title: 'Error',
        description: errorMessage,
      })
    }
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* form fields */}
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Saving...' : 'Save'}
      </button>
    </form>
  )
}

Mutation Hook Options

Mutation hooks return several useful properties:
const {
  mutate,           // Trigger mutation (no await)
  mutateAsync,      // Trigger mutation (returns promise)
  isPending,        // Is mutation in progress
  isError,          // Did mutation fail
  isSuccess,        // Did mutation succeed
  error,            // Error object if failed
  data,             // Response data
  reset             // Reset mutation state
} = useUpdateUser()

Synchronous Mutation

const { mutate, isPending } = useUpdateUser()

mutate(
  { updateUserId: userId, input: { firstName: 'John' } },
  {
    onSuccess: (data) => {
      console.log('Success:', data)
    },
    onError: (error) => {
      console.error('Error:', error)
    }
  }
)

Asynchronous Mutation

const { mutateAsync } = useUpdateUser()

try {
  const result = await mutateAsync({
    updateUserId: userId,
    input: { firstName: 'John' }
  })
  console.log('Success:', result)
} catch (error) {
  console.error('Error:', error)
}

File Upload Mutations

Some mutations support file uploads:
apps/console/src/lib/graphql-hooks/user.ts
export const useUpdateUserAvatar = () => {
  const { queryClient } = useGraphQLClient()

  return useMutation({
    mutationFn: (payload: UpdateUserMutationVariables) =>
      fetchGraphQLWithUpload({
        query: UPDATE_USER,
        variables: payload
      }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['user'] })
    },
  })
}
Usage:
const { mutateAsync: updateAvatar } = useUpdateUserAvatar()

const file = document.querySelector('input[type="file"]').files[0]

await updateAvatar({
  updateUserId: userId,
  input: { displayName: 'John' },
  avatarFile: file
})

Cache Invalidation

Mutations automatically invalidate related queries:
apps/console/src/lib/graphql-hooks/user.ts
export const useUpdateUser = () => {
  const { client, queryClient } = useGraphQLClient()
  
  return useMutation<UpdateUserMutation, unknown, UpdateUserMutationVariables>({
    mutationFn: async (payload) => client.request(UPDATE_USER, payload),
    onSuccess: () => {
      // Invalidate all user queries to refetch fresh data
      queryClient.invalidateQueries({ queryKey: ['user'] })
    },
  })
}

Invalidation Patterns

// Invalidate all queries with this key
queryClient.invalidateQueries({ queryKey: ['user'] })

// Invalidate specific query
queryClient.invalidateQueries({ queryKey: ['user', userId] })

// Invalidate multiple keys
queryClient.invalidateQueries({ queryKey: ['organizations'] })
queryClient.invalidateQueries({ queryKey: ['organizationsWithMembers'] })

Optimistic Updates

Update UI immediately before server responds:
const { mutate } = useUpdateUser()
const queryClient = useQueryClient()

mutate(
  { updateUserId: userId, input: { firstName: 'John' } },
  {
    onMutate: async (variables) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: ['user', userId] })

      // Snapshot previous value
      const previousUser = queryClient.getQueryData(['user', userId])

      // Optimistically update
      queryClient.setQueryData(['user', userId], (old) => ({
        ...old,
        user: { ...old.user, firstName: variables.input.firstName }
      }))

      return { previousUser }
    },
    onError: (err, variables, context) => {
      // Rollback on error
      queryClient.setQueryData(
        ['user', userId],
        context.previousUser
      )
    },
    onSettled: () => {
      // Refetch after error or success
      queryClient.invalidateQueries({ queryKey: ['user', userId] })
    },
  }
)

Error Handling

Handle GraphQL errors gracefully:
import { parseErrorMessage } from '@/utils/graphQlErrorMatcher'

const { mutateAsync } = useUpdateUser()

try {
  await mutateAsync(variables)
} catch (error) {
  const errorMessage = parseErrorMessage(error)
  // errorMessage: "Email already exists" or "Validation failed"
  toast.error(errorMessage)
}

Response Extensions

Some mutations return custom extensions:
apps/console/src/lib/graphql-hooks/organization.ts
type CreateOrgExtensions = {
  auth?: {
    access_token: string
    refresh_token: string
    authorized_organization: string
  }
}

export const useCreateOrganization = () => {
  const { client, queryClient } = useGraphQLClient()

  return useMutation<
    { data: CreateOrganizationMutation; extensions?: CreateOrgExtensions },
    unknown,
    CreateOrganizationMutationVariables
  >({
    mutationFn: async (input) => {
      const response = await client.rawRequest<
        CreateOrganizationMutation,
        CreateOrganizationMutationVariables
      >(CREATE_ORGANIZATION, input)

      return {
        data: response.data,
        extensions: response.extensions as CreateOrgExtensions | undefined,
      }
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['organizationsWithMembers'] })
    },
  })
}
Usage:
const { mutateAsync: createOrg } = useCreateOrganization()

const result = await createOrg({ input: { name: 'acme' } })

if (result.extensions?.auth) {
  // Update auth tokens
  setTokens(result.extensions.auth)
}

Best Practices

Handle All States

Always handle pending, error, and success states in your UI.

Invalidate Related Queries

Invalidate query cache after mutations to keep data fresh.

Use Optimistic Updates

For better UX, update the UI optimistically before the server responds.

Parse Error Messages

Use utility functions to extract user-friendly error messages from GraphQL errors.

Disable During Submission

Disable form submission while mutations are pending to prevent duplicate requests.

Show User Feedback

Always show success/error notifications after mutations complete.

Build docs developers (and LLMs) love