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
- Verify the criterion_id belongs to the current rubric's criteria before calling regenerate_criteria_via_llm.
- Re-fetch the rubric (and its fresh criteria ids) before issuing a regeneration request after any save/regeneration.
- If the criterion should exist, check that the rubric record passed to RubricLlmService.new is the up-to-date persisted rubric.
- 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
- Always re-fetch fresh criterion ids from the server after any rubric save or regeneration.
- Never cache criterion ids across rubric versions in the client.
- Validate criterion_id against rubric.criteria before invoking the service.
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
- Cannot regenerate criteria with learning outcomes attached
- LLM generation is only available for rubrics associated…
- Rubric not found: #
- Submission not found
- The AI response was not in the expected format. Please try…
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)