instructure/canvas-lms · error · ActiveRecord::RecordNotSaved

Cannot unarchive a deleted LearningOutcomeGroup

Error message

Cannot unarchive a deleted LearningOutcomeGroup

What it means

LearningOutcomeGroup#unarchive! only restores groups whose workflow_state is 'archived'. If the group is in the 'deleted' state, calling unarchive! raises ActiveRecord::RecordNotSaved because un-deleting via unarchive! is not a supported state transition. Deleted groups must be restored through the restore/delete workflow, not unarchive!.

Solutions

  1. Check workflow_state before calling unarchive!: only invoke it when state is 'archived'.
  2. To restore a deleted group, use the appropriate restore mechanism (e.g. restore method / undestroy) rather than unarchive!.
  3. If the group should not exist, skip it instead of trying to unarchive.

Example fix

// before
group.unarchive!
// after
group.unarchive! if group.workflow_state == "archived"
Defensive patterns

Strategy: validation

Validate before calling

raise "cannot unarchive deleted group" unless group.workflow_state == "archived"
group.unarchive!

Type guard

def unarchivable?(group) = group.workflow_state == "archived"

Try / catch

begin
  group.unarchive!
rescue ActiveRecord::RecordNotSaved => e
  Rails.logger.warn("#{e.message} (state=#{group.workflow_state})")
end

Prevention

When it happens

Trigger: Calling LearningOutcomeGroup#unarchive! on a record where workflow_state == 'deleted' (e.g. after group.destroy or soft delete), instead of on an archived group.

Common situations: Restoring outcome groups after an account/course cleanup; UI code or scripts that blanket-call unarchive! on all outcome groups of a subtree; data migrations that archived and later deleted groups.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

Thrown at app/models/learning_outcome_group.rb:290

  def archive!
    # Only active groups can be archived
    if workflow_state == "active"
      self.workflow_state = "archived"
      self.archived_at = Time.now.utc
      save!
    elsif workflow_state == "deleted"
      raise ActiveRecord::RecordNotSaved, "Cannot archive a deleted LearningOutcomeGroup"
    end
  end

  def unarchive!
    # Only archived groups can be unarchived
    if workflow_state == "archived"
      self.workflow_state = "active"
      self.archived_at = nil
      save!
    elsif workflow_state == "deleted"
      raise ActiveRecord::RecordNotSaved, "Cannot unarchive a deleted LearningOutcomeGroup"
    end
  end

  scope :active, -> { where("learning_outcome_groups.workflow_state NOT IN ('deleted', 'archived')") }
  scope :active_first, -> { order(Arel.sql("CASE WHEN workflow_state = 'active' THEN 0 ELSE 1 END")) }
  scope :archived, -> { where("learning_outcome_groups.workflow_state = 'archived' AND learning_outcome_groups.archived_at IS NOT NULL") }

  scope :global, -> { where(context_id: nil) }

  scope :root, -> { where(learning_outcome_group_id: nil) }

  def self.for_context(context)
    context ? context.learning_outcome_groups : LearningOutcomeGroup.global
  end

  def self.find_or_create_root(context, force)
    scope = for_context(context)
    # do this in a transaction, so parallel calls don't create multiple roots

View on GitHub (pinned to 1c9f0bb801)