instructure/canvas-lms · error · RuntimeError

Unable to find external tool to import content.

Error message

Unable to find external tool to import content.

What it means

Raised by Lti::ContentMigrationService::Importer#load_tool! when the original ContextExternalTool exists but Lti::ToolFinder.from_url cannot resolve a tool for the course whose content_migration is configured, or the resolved tool lacks content_migration_configured?. It means Canvas cannot find a valid external tool in the target course to send the imported content to.

Solutions

  1. Install/re-install the external tool in the target course's account (or course) so ToolFinder can resolve it by domain/url.
  2. Verify the tool's configuration includes the content_migration settings (import_start_url) — re-add them in the tool XML or app settings.
  3. Check the tool is available in the target course's context and not disabled at account/sub-account level.
  4. Confirm @original_tool_id points to an existing ContextExternalTool on this Canvas instance (not a stale ID from another environment).
  5. Use preferred_tool_id matching (reinstalled tools) or re-copy the course after the tool is properly configured.

Example fix

# before: import into account lacking the tool
# "Unable to find external tool to import content."
# after: install the tool in the destination account first, then re-run the import
tool = account.context_external_tools.create!(name: 'Vendor Tool', domain: 'tool.example.com', settings: { content_migration: { import_start_url: 'https://tool.example.com/import' } })
Defensive patterns

Strategy: validation

Validate before calling

# check before calling send_imported_content
original_tool = ContextExternalTool.find_by(id: original_tool_id)
resolved = original_tool && Lti::ToolFinder.from_url(original_tool.domain, course, preferred_tool_id: original_tool_id)
unless resolved&.content_migration_configured?
  raise "Tool #{original_tool_id} is not installed/configured for content migration in #{course.name}; install it first."
end

Type guard

def tool_ready_for_import?(course, original_tool_id)
  original_tool = ContextExternalTool.find_by(id: original_tool_id)
  return false unless original_tool
  tool = Lti::ToolFinder.from_url(original_tool.domain, course, preferred_tool_id: original_tool_id)
  !tool.nil? && tool.content_migration_configured?
end

Try / catch

begin
  importer.send_imported_content(course, migration, content)
rescue RuntimeError => e
  if e.message == 'Unable to find external tool to import content.'
    # prompt admin to install the tool in the destination account, or skip this tool's content
    flag_missing_tool(original_tool_id)
  else
    raise
  end
end

Prevention

When it happens

Trigger: Calling send_imported_content with an original_tool_id whose tool (a) was deleted (ContextExternalTool.find raises RecordNotFound upstream or the domain lookup fails), (b) is not installed/available in the target course's account chain, or (c) is found but has no content_migration configuration (no import settings in its tool XML/settings).

Common situations: Importing a course copy into a different Canvas account/instance where the external tool was never installed; the tool was uninstalled or replaced between export and import (tool reinstallation changes IDs); tool configuration XML updated and the content_migration block removed; admin disabled the tool in the target course.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


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

Appendix: source

Thrown at app/models/lti/content_migration_service/importer.rb:102

      end

      private

      def start_import_post_body(content)
        {
          context_id: Lti::V1p1::Asset.opaque_identifier_for(@course),
          data: content,
          tool_consumer_instance_guid: @root_account.lti_guid,
        }.merge(expanded_variables)
      end

      def load_tool!
        return @tool if @tool

        original_tool = ContextExternalTool.find(@original_tool_id)
        @tool = Lti::ToolFinder.from_url(original_tool.domain, @course, preferred_tool_id: @original_tool_id).tap do |t|
          unless t&.content_migration_configured?
            raise "Unable to find external tool to import content."
          end
        end
      end

      def import_start_url
        @import_url ||= @tool.settings.dig(:content_migration, :import_start_url)
      end

      def import_format
        @tool.settings.dig(:content_migration, :import_format)
      end
    end
  end
end

View on GitHub (pinned to 1c9f0bb801)