instructure/canvas-lms · error · GraphQL::ExecutionError

Enrollment is not in invited state

Error message

Enrollment is not in invited state

What it means

The mutation only accepts invitations in the 'invited' workflow_state; otherwise it raises 'Enrollment is not in invited state'. Accepting is a state transition, so enrollments already accepted, declined, rejected, completed, or deleted cannot pass through accept! again.

Solutions

  1. Check the enrollment's current workflow_state before calling (or fetch the enrollment via GraphQL/API) and treat already-accepted as success.
  2. Make the caller idempotent: persist locally that the invitation was accepted and skip repeat calls.
  3. If the invitation was declined/expired, re-invite the user to generate a new enrollment.
  4. Investigate enrollment state transitions (concluded course, deleted enrollment) if the state changed unexpectedly.
  5. On the server side you could accept or return success for already-accepted enrollments instead of erroring, for idempotency.

Example fix

// before
if (!enrollment.invited?) raise GraphQL::ExecutionError, "Enrollment is not in invited state"
// after
if (enrollment.accepted?) return { enrollment:, success: true } # idempotent
raise GraphQL::ExecutionError, "Enrollment is not in invited state" unless enrollment.invited?
Defensive patterns

Strategy: fallback

Validate before calling

// make the call idempotent: check state first, skip if already accepted
if (locallyAccepted.has(enrollmentUuid)) return { success: true }

Try / catch

try {
  return await client.request(ACCEPT_INVITATION, { enrollmentUuid })
} catch (e) {
  if (/not in invited state/i.test(e.message))
    return { success: true, alreadyHandled: true } // treat as done
  throw e
}

Prevention

When it happens

Trigger: Double-clicking accept or retrying the mutation after a first successful accept; calling it on a previously declined invitation; course concluded/invitation expired so the enrollment moved out of 'invited'; batch scripts re-running on already-processed enrollments.

Common situations: Users replaying invitation links from email after already accepting; automated sync jobs lacking idempotency; race conditions where two tabs accept simultaneously and the second sees a non-invited state.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/0100a2e2027991e1. Report an issue: GitHub.

Appendix: source

Thrown at app/graphql/mutations/accept_enrollment_invitation.rb:39

module Mutations
  class AcceptEnrollmentInvitation < BaseMutation
    argument :enrollment_uuid, String, required: true

    field :enrollment, Types::EnrollmentType, null: true
    field :success, Boolean, null: false

    def resolve(input:, **)
      user = context[:current_user]
      raise GraphQL::ExecutionError, I18n.t("Must be logged in") unless user

      enrollment = Enrollment.where(uuid: input[:enrollment_uuid]).first
      raise GraphQL::ExecutionError, I18n.t("Enrollment invitation not found") unless enrollment

      # Verify the enrollment belongs to the current user
      raise GraphQL::ExecutionError, I18n.t("Unauthorized") unless enrollment.user == user

      # Verify the enrollment is in invited state
      raise GraphQL::ExecutionError, I18n.t("Enrollment is not in invited state") unless enrollment.invited?

      begin
        if enrollment.accept!
          {
            enrollment:,
            success: true
          }
        else
          {
            enrollment: nil,
            success: false,
            errors: [{ message: I18n.t("Failed to accept enrollment invitation") }]
          }
        end
      rescue => e
        {
          enrollment: nil,
          success: false,

View on GitHub (pinned to 1c9f0bb801)