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
- 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.
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
- Always branch on workflow_state before calling unarchive!
- Use restore-style APIs for deleted records, unarchive! only for archived ones
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
- Cannot archive a deleted LearningOutcome
- Cannot archive a deleted LearningOutcomeGroup
- Cannot unarchive a deleted LearningOutcome
- A course did not pass validation
- A # user did not pass validation (user: # , # : # , error…
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 rootsView on GitHub (pinned to 1c9f0bb801)