instructure/canvas-lms · error · LastLinkToOutcomeNotDestroyed

Outcome '# ' cannot be deleted because it is aligned to…

Error message

Outcome '#{aligned_outcome}' cannot be deleted because it is aligned to content.

What it means

ContentTag#destroy raises LastLinkToOutcomeNotDestroyed when can_destroy? is false, i.e. the tag is the last alignment keeping the learning outcome attached to content. Destroying it would orphan an outcome that is still aligned, so Canvas blocks the soft delete and names the outcome in the message.

Solutions

  1. Remove or replace other alignments for the outcome first so this tag is no longer the last link, then retry destroy.
  2. If the outcome itself should go, delete the learning outcome through the outcomes API instead of destroying its alignment tag.
  3. Guard the delete with can_destroy? and show the outcome's short_description in a user-facing message.

Example fix

// before
content_tag.destroy
// after
if content_tag.can_destroy?
  content_tag.destroy
else
  outcome = content_tag.learning_outcome
  raise UserFacingError, "Outcome '#{outcome.short_description}' is still aligned to content"
end
Defensive patterns

Strategy: validation

Validate before calling

if content_tag.respond_to?(:can_destroy?) && !content_tag.can_destroy?
  raise UserFacingError, "Outcome '#{content_tag.learning_outcome.short_description}' is still aligned"
end

Try / catch

begin
  content_tag.destroy
rescue LastLinkToOutcomeNotDestroyed => e
  flash[:error] = e.message
end

Prevention

When it happens

Trigger: Deleting a module item / alignment whose content_tag is the final link between its learning_outcome and content; deleting outcomes via migrations or API calls that remove the last alignment tag.

Common situations: Cleaning up course modules that still host an assessed outcome; outcome import/export jobs removing alignments; teachers deleting a module containing the only aligned item for an outcome.

Related errors


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

Appendix: source

Thrown at app/models/content_tag.rb:472

          alignment_conditions[:context_id] = context_id
          alignment_conditions[:context_type] = context_type
        end

        @active_alignment_tags = ContentTag.learning_outcome_alignments.active.where(alignment_conditions)
        if @active_alignment_tags.exists?
          # then don't let them delete the link
          return false
        end
      end
    end
    true
  end

  alias_method :destroy_permanently!, :destroy
  def destroy
    unless can_destroy?
      aligned_outcome = @active_alignment_tags.map(&:learning_outcome).first.short_description
      raise LastLinkToOutcomeNotDestroyed, "Outcome '#{aligned_outcome}' cannot be deleted because it is aligned to content."
    end

    context_module&.remove_completion_requirement(id)

    self.workflow_state = "deleted"
    save!

    # for outcome links delete the associated friendly description
    delete_outcome_friendly_description if content_type == "LearningOutcome"

    run_submission_lifecycle_manager_for_quizzes_next(force: true)
    update_module_item_submissions(change_of_module: false)

    # after deleting the last native link to an unaligned outcome, delete the
    # outcome. we do this here instead of in LearningOutcome#destroy because
    # (a) LearningOutcome#destroy *should* only ever be called from here, and
    # (b) we've already determined other_link and native
    if @should_destroy_outcome

View on GitHub (pinned to 1c9f0bb801)