instructure/canvas-lms · error · InvalidDataError

Points for tier must be less than points for prior tier (…

Error message

Points for tier %{index} must be less than points for prior tier (%{points} is greater than %{prior})

What it means

Raised by Outcomes::CsvImporter#parse_ratings when a tier's points value is greater than the previous tier's points. Outcome ratings must be in descending point order (first tier is highest); each parsed points value must be <= the prior one or an InvalidDataError reports both values. This preserves Canvas's expectation that mastery ratings go from highest to lowest.

Solutions

  1. Reverse the rating order so points descend: highest tier (e.g. Exemplary) first.
  2. Re-sort the ratings pairs by points descending before importing.
  3. Verify each tier's points is less than or equal to the previous tier's points.

Example fix

// before
ratings: 1,Beginning,3,Meets,5,Exemplary
// after
ratings: 5,Exemplary,3,Meets,1,Beginning
Defensive patterns

Strategy: validation

Validate before calling

points = cells.each_slice(2).map(&:first).compact.map(&:to_f)
raise 'rating points must descend' unless points.each_cons(2).all? { |a, b| a >= b }

Try / catch

begin
  importer.run
rescue Outcomes::Import::InvalidDataError => e
  errors << "Row #{i}: #{e.message}"
end

Prevention

When it happens

Trigger: Ratings written in ascending order, e.g. `3,Meets,5,Exemplary` — at tier 2, points 5 > prior 3; duplicated or shuffled point values; users listing tiers from lowest to highest mastery.

Common situations: Authors thinking of ratings as ascending rubric levels; reordering tiers in a spreadsheet without re-sorting points; converting from tools that use ascending scales.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


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

Appendix: source

Thrown at lib/outcomes/csv_importer.rb:179

      if object[:mastery_points].present?
        object[:mastery_points] = strict_parse_float(object[:mastery_points], I18n.t("mastery points"))
      end
      if object[:friendly_description].present? && object[:friendly_description].length > 255
        raise InvalidDataError, I18n.t("Friendly description is too long (maximum is 255 characters)")
      end

      import_object(object)
    end

    def parse_ratings(ratings)
      prior = nil
      drop_trailing_nils(ratings).each_slice(2).to_a.map.with_index(1) do |(points, description), index|
        raise InvalidDataError, I18n.t("Points for rating tier %{index} not present", index:) if points.nil? || points.blank?

        points = strict_parse_float(points, I18n.t("rating tier %{index} threshold", index:))

        if prior.present? && prior < points
          raise InvalidDataError, I18n.t(
            "Points for tier %{index} must be less than points for prior tier (%{points} is greater than %{prior})",
            index:,
            prior:,
            points:
          )
        end

        prior = points
        { points:, description: }
      end
    end

    def normalize_i18n(string)
      raise ArgumentError if string.blank?

      separator = I18n.t("number.format.separator")
      delimiter = I18n.t("number.format.delimiter")
      string.gsub(delimiter, "").gsub(separator, ".")

View on GitHub (pinned to 1c9f0bb801)