instructure/canvas-lms · error · InvalidDataError

Cyclic reference detected when importing

Error message

Cyclic reference detected when importing: %{vendor_guid}

What it means

find_parents guards against outcome group hierarchy cycles. When importing a row that specifies an explicit learning_outcome_group_id parent and the model already exists, if the proposed parent group has the model itself among its ancestors, making it the parent would create a cycle in the group tree, so the import raises this InvalidDataError.

Solutions

  1. Inspect the group tree and remove the row (or change learning_outcome_group_id) so no group becomes its own ancestor.
  2. Reorder the import so parents are imported before children and parent_guids reflect the desired final tree.
  3. First move the affected group out of the cycle in the UI/API, then re-run the import.

Example fix

// before: group 'A' row declares parent = group 'B', but B is a child of A
{
  "vendor_guid": "A",
  "learning_outcome_group_id": <B id>
}
// after: parent A at the top level
{
  "vendor_guid": "A"
}
Defensive patterns

Strategy: validation

Validate before calling

// Ruby: ensure no cycle before setting the parent
def safe_parent?(group, parent_group)
  group.new_record? || !parent_group.ancestor_ids.member?(group.id)
end

Try / catch

begin
  importer.import_object(...)
rescue Outcomes::Import::InvalidDataError => e
  raise unless e.message.include?('Cyclic reference')
  # fix parent_guids / learning_outcome_group_id and retry
end

Prevention

When it happens

Trigger: Importing group A with learning_outcome_group_id pointing to a group B where B.ancestor_ids includes A's id (e.g. setting A's parent to its own descendant or itself); lib/outcomes/import.rb:298, called from import_group/import_outcome.

Common situations: Hand-edited import files rearranging group nesting; round-tripped exports where group order changed so a child is processed before its parent's parent; scripted re-parenting that forgot the tree is acyclic.

Related errors


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

Appendix: source

Thrown at lib/outcomes/import.rb:298

    def create_rubric(ratings, mastery_points)
      rubric = {}
      rubric[:enable] = true
      rubric[:mastery_points] = mastery_points
      rubric[:ratings] = ratings.map.with_index { |v, i| [i, v] }.to_h
      rubric
    end

    def root_parent(given_context)
      @root_parents ||= {}
      @root_parents[given_context] ||= LearningOutcomeGroup.find_or_create_root(given_context, true)
    end

    def find_parents(object, given_context, allow_indirect: false, model: nil)
      if !model.nil? && !model.new_record? && object[:learning_outcome_group_id]
        parent_group = LearningOutcomeGroup.find(object[:learning_outcome_group_id])
        if parent_group.ancestor_ids.member?(model.id)
          raise InvalidDataError, I18n.t(
            "Cyclic reference detected when importing: %{vendor_guid}",
            vendor_guid: object[:vendor_guid]
          )
        end
      end
      if object[:parent_guids].nil? || object[:parent_guids].blank?
        group = [LearningOutcomeGroup.find(object[:learning_outcome_group_id])] if object[:learning_outcome_group_id]
        group ||= [root_parent(given_context)]

        return group
      end

      guids = object[:parent_guids].strip.split.uniq
      possible_parents = LearningOutcomeGroup.where(outcome_import_id:, vendor_guid: guids)

      # If allow_indirect is true, we could `filter{|g| child_context?(g.context) }`, but it is costly and
      # redundant (since outcome_import_id matching is already enforced)
      possible_parents = possible_parents.where(context: given_context) unless allow_indirect

View on GitHub (pinned to 1c9f0bb801)