instructure/canvas-lms · error · ImportError
Cannot delete the sub_account with ID: #
Error message
Cannot delete the sub_account with ID: #{account_id} because it has active sub accounts. What it means
When an account import requests status 'deleted', the importer checks account.sub_accounts.active.exists? and refuses to delete an account that still has active child accounts, raising ImportError. Canvas requires bottom-up deletion of the account hierarchy.
Solutions
- Include 'deleted' rows for all active child accounts in the same SIS batch, ordered before the parent.
- Manually delete or deactivate the child accounts in Canvas first.
- Re-check the account tree in the Canvas UI for remaining active sub-accounts.
Example fix
// before
# accounts.csv: delete parent while child stays active
add_account('parent1', nil, 'deleted', 'Parent', nil)
// after
add_account('child1', 'parent1', 'deleted', 'Child', nil)
add_account('parent1', nil, 'deleted', 'Parent', nil) Defensive patterns
Strategy: try-catch
Validate before calling
if status.to_s =~ /deleted/i acct = root_account.all_accounts.find_by(sis_source_id: account_id) raise "delete children first" if acct&.sub_accounts&.active&.exists? end
Try / catch
begin
importer.add_account(id, parent_id, 'deleted', name, integration_id)
rescue SIS::BaseImporter::ImportError => e
logger.warn("deferring delete: #{e.message}")
retry_later
end Prevention
- Emit deletion rows depth-first (leaves before parents)
- Do a dry-run pass computing which deletions are blocked
- Reconcile account tree state before batch runs
When it happens
Trigger: add_account with status 'deleted' for an account that has at least one active (non-deleted workflow_state) sub-account in the database.
Common situations: Deleting a whole branch of the account tree from SIS without ordering leaves first; partial imports where children were not deleted in the same or prior run.
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 delete the sub_account with ID: #
- A cross-listing referenced a non-existent section #
- A deleted cross-listing failed: #
- A new_id, '# ', referenced an existing # and the # with #…
- A new_integration_id, '#
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/b88a9413ccbc9a7e.
Report an issue: GitHub.
Appendix: source
Thrown at lib/sis/account_importer.rb:90
account ||= @root_account.sub_accounts.new
account.root_account = @root_account
if account.new_record? || !account.stuck_sis_fields.include?(:parent_account_id) || Account.sis_stickiness_options[:add_sis_stickiness]
account.parent_account = parent || @root_account
end
# only update the name on new records, and ones that haven't been changed since the last sis import
account.name = name if name.present? && (account.new_record? || !account.stuck_sis_fields.include?(:name))
account.integration_id = integration_id if integration_id.present?
account.sis_source_id = account_id
if status.present?
case status
when /active/i
account.workflow_state = "active"
when /deleted/i
raise ImportError, "Cannot delete the sub_account with ID: #{account_id} because it has active sub accounts." if account.sub_accounts.active.exists?
raise ImportError, "Cannot delete the sub_account with ID: #{account_id} because it has active courses." if account.courses.active.exists?
account.workflow_state = "deleted"
end
end
@accounts_cache[account.sis_source_id] = account
unless account.changed?
@success_count += 1
accounts_to_set_sis_batch_ids << account.id unless account.sis_batch_id == @batch.try(:id)
return
end
account.sis_batch_id = @batch.id
update_account_associations = account.root_account_id_changed? || account.parent_account_id_changed?
if account.saveView on GitHub (pinned to 1c9f0bb801)