instructure/canvas-lms · error · SubmissionError

no matching external tool found

Error message

no matching external tool found

What it means

In CreateSubmissionDraft, when the submission type is basic_lti_launch the mutation resolves the configured LTI tool via Lti::ToolFinder.from_url using the provided lti_launch_url. If no installed ContextExternalTool in the course matches that URL (and preferred_tool_id), it raises SubmissionError with "no matching external tool found".

Solutions

  1. Confirm an external tool with a URL matching lti_launch_url is installed in the course (or its account), e.g. check course.context_external_tools.
  2. Pass the correct external_tool_id matching the deployed tool.
  3. Fix the lti_launch_url to exactly match the tool's configured domain/path.
  4. If the tool config changed, re-deploy/re-install the external tool (XML or LTI 1.3 registration) for this course or account.

Example fix

// before
mutation.draft(submissionId: id, ltiLaunchUrl: 'https://old-tool.example.com/launch')
// after
# ensure tool installed, then pass matching id
mutation.draft(submissionId: id, ltiLaunchUrl: 'https://tool.example.com/launch', externalToolId: tool.id)
Defensive patterns

Strategy: validation

Validate before calling

const tools = course.contextExternalTools.nodes || []
const match = tools.find(t => ltiLaunchUrl.includes(t.domain))
if (!match) throw new SkipError('no matching external tool for ' + ltiLaunchUrl)

Type guard

function hasMatchingTool(tools, url) { return Array.isArray(tools) && tools.some(t => typeof t.domain === 'string' && url.includes(t.domain)); }

Try / catch

try {
  await createSubmissionDraft(...)
} catch (e) {
  if (e.message.includes('no matching external tool found')) {
    showLtiToolMissingDialog(ltiLaunchUrl)
  } else throw e;
}

Prevention

When it happens

Trigger: Submitting a draft with submission_types including basic_lti_launch and an lti_launch_url that no installed external tool in the course covers; passing an external_tool_id that does not match or is not deployed in the course; tool configured at account level but not visible to the course; URL changed after the tool was configured.

Common situations: LTI 1.3 tool moved to a new launch URL without updating Canvas config; developer uses a dev/test tool URL on a different environment; tool deleted or uninstalled from the course/account; typo or trailing path difference in the launch URL.

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.

Related errors


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

Appendix: source

Thrown at app/graphql/mutations/create_submission_draft.rb:70

      submission:,
      submission_attempt: input[:attempt] || (submission.attempt + 1)
    ).first_or_create!

    # TODO: we should research if we should split this mutation into a separate
    #       mutation for each draft type. the primary concern is the confusion
    #       of ignoring potentially included input types if they don't match
    #       the active submission.
    submission_draft.active_submission_type = input[:active_submission_type]
    case input[:active_submission_type]
    when "basic_lti_launch"
      raise SubmissionError if input[:lti_launch_url].blank? || input[:external_tool_id].blank?

      external_tool = Lti::ToolFinder.from_url(
        input[:lti_launch_url],
        submission.course,
        preferred_tool_id: input[:external_tool_id]
      )
      raise SubmissionError, I18n.t("no matching external tool found") if external_tool.blank?

      submission_draft.context_external_tool_id = external_tool.id
      submission_draft.lti_launch_url = input[:lti_launch_url]
      submission_draft.resource_link_lookup_uuid = input[:resource_link_lookup_uuid]
    when "media_recording"
      submission_draft.media_object_id = input[:media_id]
    when "online_text_entry"
      submission_draft.body = input[:body]
    when "online_upload"
      file_ids = (input[:file_ids] || []).compact.uniq
      attachments = get_and_verify_attachments!(file_ids)
      verify_allowed_extensions!(submission.assignment, attachments)
      submission_draft.attachments = attachments
    when "online_url"
      submission_draft.url = input[:url]
    end

    submission_draft.save!

View on GitHub (pinned to 1c9f0bb801)