instructure/canvas-lms · error

Cannot regenerate criteria with learning outcomes attached

Error message

Cannot regenerate criteria with learning outcomes attached

What it means

RubricLlmService#regenerate_criteria_via_llm refuses to regenerate a single criterion that is tied to a learning outcome (criterion[:learning_outcome_id] present). Outcome criteria are managed by the outcomes system, so LLM rewriting them would desync the rubric from the outcome. The caller must exclude such criteria from regeneration.

Solutions

  1. Filter out criteria with learning_outcome_id present before offering/sending regeneration requests.
  2. In the UI, hide or disable the regenerate action for outcome-linked criteria.
  3. If regeneration of outcome criteria is truly needed, regenerate a non-outcome criterion or detach the outcome first (with user awareness of consequences).

Example fix

// before
service.regenerate_criteria_via_llm(..., { criterion_id: outcome_linked_criterion[:id] })
// after
if outcome_linked_criterion[:learning_outcome_id].present?
  return render json: { error: 'cannot regenerate outcome criterion' }, status: :unprocessable_entity
end
service.regenerate_criteria_via_llm(..., { criterion_id: outcome_linked_criterion[:id] })
Defensive patterns

Strategy: validation

Validate before calling

criterion = rubric.criteria.find { |c| c[:id].to_s == criterion_id.to_s }
return :skip if criterion && criterion[:learning_outcome_id].present?

Type guard

def outcome_linked?(criterion)
  criterion.is_a?(Hash) && criterion[:learning_outcome_id].present?
end

Try / catch

begin
  service.regenerate_criteria_via_llm(...)
rescue RuntimeError => e
  raise unless e.message == 'Cannot regenerate criteria with learning outcomes attached'
  notify_user('outcome-linked criteria cannot be regenerated')
end

Prevention

When it happens

Trigger: Calling regenerate_criteria_via_llm with regenerate_options[:criterion_id] pointing at a criterion whose learning_outcome_id is set — i.e. an outcome-aligned rubric row selected for regeneration in the rubric editor.

Common situations: User clicks a regenerate control on a criterion that Canvas linked to a learning outcome; API client lists all criteria ids including outcome-linked ones; UI failed to disable regenerate for outcome rows.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

Thrown at app/services/rubric_llm_service.rb:119

  #   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?
        end
      end
    end

View on GitHub (pinned to 1c9f0bb801)