The has_submitted_submissions API trap
The Canvas Assignments API has a boolean called has_submitted_submissions, and it does not mean what it looks like it means: it flips true when any student in the course submits, and says nothing about you. Instructure's own docs state it — "submitted to by at least one student." Any app that reads it as "the current user submitted" will mark your assignment done because a classmate turned theirs in. We know because we shipped exactly that bug in Margin's early Canvas sync, and removed it; the correct per-user signal is the submission include.
What you see
If you are a student, you see it second-hand: a planner or dashboard built on this field can close an assignment you have not started, because the trigger is a classmate's submission rather than yours. Being told you are finished by something that never looked at your work is exactly the wrong kind of reassurance.
If you are the developer of that app, you see a field named in the plainest possible way — has_submitted_submissions — sitting right on the assignment object you already fetched, requiring no extra include and no extra request. It reads as a free answer to "is this done?" It is not.
Why it happens
The value is computed across the whole assignment, not for the person asking. In Canvas's own source, abstract_assignment.rb defines it as:
def has_submitted_submissions?
return @has_submitted_submissions unless @has_submitted_submissions.nil?
submitted_count > 0
end
submitted_count counts submissions on the assignment, across everyone enrolled, and the boolean is whether that count is above zero. One submission anywhere in the course flips the boolean for everyone who reads it. Instructure's Assignments API reference says so directly in the field description: "If true, the assignment has been submitted to by at least one student"
Whatever it was added for, it is not scoped to the person asking. Read from a student-scoped token it is still true, still course-wide, and still silent about the person holding the token.
What you can do about it
If you are building on the Canvas API: ask for the per-user object instead. On the Assignments endpoint, include[]=submission returns "The current user's current +Submission+" (the plus signs are the doc's own markup) — documented on the same page as the trap. Read that object's workflow_state and submitted_at, and treat a missing submission object as unknown, not as done.
Two habits worth adopting alongside it:
- Never close a task on an absence. An item leaving a feed is not evidence it was submitted — Canvas drops items from its to-do feed for several reasons that have nothing to do with completion.
- Name the trap in a code comment where you fixed it. This field is attractive enough that someone will reintroduce it during a refactor if the reason for avoiding it is not written down next to it.
If you are a student whose planner did this: check the assignment page in Canvas itself, or the gradebook, before believing any third-party app that says you are done. The submission record on Canvas is the source of truth; a synced tool is a convenience layer over it. Margin shows what Canvas exposes — check your syllabus too.
Evidence
- Instructure Assignments API reference (developerdocs.instructure.com/services/canvas/resources/assignments),
has_submitted_submissionsfield description, retrieved 2026-07-30: "If true, the assignment has been submitted to by at least one student" The same page documents thesubmissioninclude as "The current user's current +Submission+" (the+…+markers are the doc's own markup). - canvas-lms,
app/models/abstract_assignment.rb(master, retrieved 2026-07-30):has_submitted_submissions?returnssubmitted_count > 0. - Margin's own code (first-person, our repo): our early Canvas sync read this boolean as a per-user completion signal — a failure mode in which one classmate submitting could close another student's still-open task. It was removed, and the current sync requires a positive per-user signal before closing anything; a comment at the call site names the trap so it is not reinstated.