instructure/canvas-lms · error · ActiveRecord::RecordNotSaved
Cannot archive a deleted LearningOutcome
Error message
Cannot archive a deleted LearningOutcome
What it means
LearningOutcome#archive! moves an active outcome to 'archived' and stamps archived_at. Archiving a deleted outcome is an invalid transition, so when workflow_state is 'deleted' it raises ActiveRecord::RecordNotSaved.
Solutions
- Guard before calling: archive! only if workflow_state == 'active'.
- Reload the record to check current state before archiving.
- Rescue ActiveRecord::RecordNotSaved and treat as a no-op when deleted.
- Permanently delete instead of archiving if that was the intent.
Example fix
// before outcome.archive! // after outcome.reload.archive! if outcome.reload.workflow_state == "active"
Defensive patterns
Strategy: try-catch
Validate before calling
outcome.reload.archive! if outcome.reload.workflow_state == "active"
Try / catch
begin
outcome.archive!
rescue ActiveRecord::RecordNotSaved
Rails.logger.info("outcome #{outcome.id} already deleted; skipping archive")
end Prevention
- Only archive outcomes verified active
- Reload before state transitions on long-lived objects
- Treat RecordNotSaved on deleted records as a no-op
When it happens
Trigger: Calling outcome.archive! on a LearningOutcome whose workflow_state is 'deleted' — e.g. acting on a stale record after another process deleted it.
Common situations: Race between deletion (importer, outcome UI) and an archive request; background jobs replaying archive on already-deleted outcomes.
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 LearningOutcomeGroup
- Cannot unarchive a deleted LearningOutcome
- Cannot unarchive a deleted LearningOutcomeGroup
- 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/b26fa2deb16fbc0c.
Report an issue: GitHub.
Appendix: source
Thrown at app/models/learning_outcome.rb:412
# in case this got called in a console, delete the alignments also. the UI
# won't (shouldn't) allow deleting the outcome if there are still
# alignments, so this will be a no-op in that case. either way, these are
# not outcome links, so ContentTag#destroy is just changing the
# workflow_state; use update_all for efficiency.
ContentTag.learning_outcome_alignments.active.where(learning_outcome_id: self).update_all(workflow_state: "deleted")
self.workflow_state = "deleted"
save!
end
def archive!
# Only active outcomes 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 LearningOutcome"
end
end
def unarchive!
# Only archived outcomes 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 LearningOutcome"
end
end
def assessed?(course = nil)
if course
learning_outcome_results.active.where(context_id: course, context_type: "Course").exists?
elsif learning_outcome_results.active.loaded?View on GitHub (pinned to 1c9f0bb801)