instructure/canvas-lms · error

Cannot find criterion with id #

Error message

Cannot find criterion with id #{criterion_id}

What it means

RubricLlmService#regenerate_criteria_via_llm raises this when a criterion_id is passed in regenerate_options but no criterion with that id exists in the rubric's criteria list (target_criterion is nil). It is a guard so LLM regeneration never silently no-ops for an unknown criterion. The id lookup happens against the rubric's loaded criteria array.

Solutions

  1. Verify the criterion_id belongs to the current rubric's criteria before calling regenerate_criteria_via_llm.
  2. Re-fetch the rubric (and its fresh criteria ids) before issuing a regeneration request after any save/regeneration.
  3. If the criterion should exist, check that the rubric record passed to RubricLlmService.new is the up-to-date persisted rubric.
  4. Handle the raised string error in the controller and return a 404-style response so the client refreshes its rubric state.

Example fix

// before
criterion_id = params[:criterion_id] # stale id from an old render
RubricLlmService.new(rubric).regenerate_criteria_via_llm(..., { criterion_id: criterion_id })
// after
criterion = rubric.criteria.find { |c| c[:id].to_s == params[:criterion_id].to_s }
return render json: { error: 'criterion not found' }, status: :not_found unless criterion
RubricLlmService.new(rubric).regenerate_criteria_via_llm(..., { criterion_id: criterion[:id] })
Defensive patterns

Strategy: validation

Validate before calling

criterion = rubric.criteria.find { |c| c[:id].to_s == criterion_id.to_s }
raise ArgumentError, "criterion #{criterion_id} not on rubric" unless criterion

Type guard

def criterion_on_rubric?(rubric, id)
  rubric.criteria.any? { |c| c[:id].to_s == id.to_s }
end

Try / catch

begin
  service.regenerate_criteria_via_llm(...)
rescue RuntimeError => e
  if e.message.start_with?('Cannot find criterion with id')
    refresh_rubric_state! # refetch rubric and criteria ids
  else
    raise
  end
end

Prevention

When it happens

Trigger: Calling regenerate_criteria_via_llm with regenerate_options[:criterion_id] set to an id that does not match any criterion on the rubric — e.g. a stale id from a previously saved rubric, an id from a different rubric, or an id after criteria were regenerated/replaced.

Common situations: Frontend caches criterion ids from an older rubric version; user regenerates criteria then re-submits an old regenerate request; cross-rubric id reuse in API integrations; typos in id passed from a controller.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at app/services/rubric_llm_service.rb:116

  # @return [Array<Hash>] normalized criteria set
  #
  # Example of text extraction format fed to LLM (rubric_to_text):
  #   criterion:c1:description="Clarity"
  #   rating:r1:description="Exemplary"
  def regenerate_criteria_via_llm(association_object, regenerate_options = {}, generate_options = {})
    validate_rubric_and_association_object(association_object)

    assignment = association_object
    generate_options = resolve_regenerate_options(generate_options, regenerate_options)
    incoming_criteria, existing_criteria_json, criteria_as_text, regenerable_criteria, learning_outcome_criteria_map, target_criterion =
      normalize_incoming_criteria(regenerate_options)

    criterion_id = regenerate_options[:criterion_id]

    # Check if trying to regenerate a learning outcome criterion (not allowed)
    if criterion_id.present?
      if target_criterion.nil?
        raise "Cannot find criterion with id #{criterion_id}"
      end
      if target_criterion[:learning_outcome_id].present?
        raise "Cannot regenerate criteria with learning outcomes attached"
      end
    end

    # If all criteria have learning outcomes, there's nothing to regenerate
    # Return the original criteria with recalculated points
    # Preserve all fields: learning_outcome_id, ignore_for_scoring, mastery_points, generated, etc.
    if regenerable_criteria.empty?
      total_points = generate_options[:total_points].to_f
      points_per_criterion = calculate_points_per_criterion(total_points, incoming_criteria.size)

      return incoming_criteria.each_with_index.map do |criterion, index|
        criterion.dup.tap do |c|
          c[:points] = points_per_criterion[index]
          # Normalize ratings from hash to array format for frontend compatibility
          c[:ratings] = normalize_ratings_array(c[:ratings]) if c[:ratings].present?

View on GitHub (pinned to 1c9f0bb801)