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
- 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.
- Pass the correct external_tool_id matching the deployed tool.
- Fix the lti_launch_url to exactly match the tool's configured domain/path.
- 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
- Keep LTI tool launch URLs in config in sync with installed tool definitions.
- Prefer passing external_tool_id alongside lti_launch_url.
- Verify tool placement includes homework_submission/assignment_selection.
- Reinstall the tool after URL changes.
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
- Unable to find external tool to import content.
- Unable to start import for external tool #
- and cannot be used together
- and cannot be used together
- A course with that id does not exist
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)