{"record":{"id":"8f7f578c95593234","repo":"instructure/canvas-lms","slug":"cannot-unarchive-a-deleted-learningoutcomegroup","errorCode":null,"errorMessage":"Cannot unarchive a deleted LearningOutcomeGroup","messagePattern":"Cannot unarchive a deleted LearningOutcomeGroup","errorType":"validation","errorClass":"ActiveRecord::RecordNotSaved","httpStatus":null,"severity":"error","filePath":"app/models/learning_outcome_group.rb","lineNumber":290,"sourceCode":"  def archive!\n    # Only active groups can be archived\n    if workflow_state == \"active\"\n      self.workflow_state = \"archived\"\n      self.archived_at = Time.now.utc\n      save!\n    elsif workflow_state == \"deleted\"\n      raise ActiveRecord::RecordNotSaved, \"Cannot archive a deleted LearningOutcomeGroup\"\n    end\n  end\n\n  def unarchive!\n    # Only archived groups can be unarchived\n    if workflow_state == \"archived\"\n      self.workflow_state = \"active\"\n      self.archived_at = nil\n      save!\n    elsif workflow_state == \"deleted\"\n      raise ActiveRecord::RecordNotSaved, \"Cannot unarchive a deleted LearningOutcomeGroup\"\n    end\n  end\n\n  scope :active, -> { where(\"learning_outcome_groups.workflow_state NOT IN ('deleted', 'archived')\") }\n  scope :active_first, -> { order(Arel.sql(\"CASE WHEN workflow_state = 'active' THEN 0 ELSE 1 END\")) }\n  scope :archived, -> { where(\"learning_outcome_groups.workflow_state = 'archived' AND learning_outcome_groups.archived_at IS NOT NULL\") }\n\n  scope :global, -> { where(context_id: nil) }\n\n  scope :root, -> { where(learning_outcome_group_id: nil) }\n\n  def self.for_context(context)\n    context ? context.learning_outcome_groups : LearningOutcomeGroup.global\n  end\n\n  def self.find_or_create_root(context, force)\n    scope = for_context(context)\n    # do this in a transaction, so parallel calls don't create multiple roots","sourceCodeStart":272,"sourceCodeEnd":308,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/learning_outcome_group.rb#L272-L308","documentation":"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!.","triggerScenarios":"Calling LearningOutcomeGroup#unarchive! on a record where workflow_state == 'deleted' (e.g. after group.destroy or soft delete), instead of on an archived group.","commonSituations":"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.","solutions":["Check workflow_state before calling unarchive!: only invoke it when state is 'archived'.","To restore a deleted group, use the appropriate restore mechanism (e.g. restore method / undestroy) rather than unarchive!.","If the group should not exist, skip it instead of trying to unarchive."],"exampleFix":"// before\ngroup.unarchive!\n// after\ngroup.unarchive! if group.workflow_state == \"archived\"","handlingStrategy":"validation","validationCode":"raise \"cannot unarchive deleted group\" unless group.workflow_state == \"archived\"\ngroup.unarchive!","typeGuard":"def unarchivable?(group) = group.workflow_state == \"archived\"","tryCatchPattern":"begin\n  group.unarchive!\nrescue ActiveRecord::RecordNotSaved => e\n  Rails.logger.warn(\"#{e.message} (state=#{group.workflow_state})\")\nend","preventionTips":["Always branch on workflow_state before calling unarchive!","Use restore-style APIs for deleted records, unarchive! only for archived ones"],"tags":["activerecord","state-machine","outcome-groups"],"backgroundTag":"invalid-state-transition","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}