All postsBuilding for US school districts · Part 8

Grade passback that lands: why the score returned 200 and vanished

The most common integration complaint in K-12 is that grades are not showing up. It is almost never an outage. Here are the four places the score actually went.

The most common integration complaint in K-12 software is some version of "the scores aren't showing up."

It's almost never an outage. The request succeeded, the platform accepted it, and the score is somewhere the teacher isn't looking.

Here are the four places it goes.

Illustration of a letter travelling through a sorting office and coming to rest in the wrong pigeonhole while a receipt is stamped successful
Delivered, receipted, and in the wrong pigeonhole. Every failure here returns a success.

Two routes, one set of problems

Scores return to a district by one of two paths.

LTI Assignment and Grade Services is the LMS route. Your tool writes to a line item in Canvas, Schoology, Moodle or Blackboard.

OneRoster gradebook is the SIS route. You write lineItems and results to the district's OneRoster endpoint.

They differ in transport and agree in every failure mode that matters, because both are ultimately writing a number into a column a teacher already has opinions about.

Failure 1: There is no line item

A line item is the gradebook column. Before you can write a score, one has to exist, and it has to be the right one.

Under AGS, the correct binding is to the resource link, the specific placement the student launched from. A teacher adds your activity as an assignment, that assignment is a resource link, and the line item belongs to it. Write to a line item you created independently and the score lands in a column the teacher didn't create, next to the one they're staring at.

Two specific mistakes:

Creating a new line item on every launch, because the lookup was written as find-or-create and the find is fragile. The gradebook accumulates a column per session and becomes unusable in a week.

Creating a line item at tool level rather than per resource link, so every assignment in the course writes into one shared column and the last score wins.

Check the lineitem claim in the launch first. If the platform gave you one, use it. That's the platform telling you exactly which column it wants. Only create a line item when you have the scope and no claim was provided, and make it idempotent on a stable key.

Failure 2: The timestamp

This one is genuinely obscure and costs teams days.

An AGS score carries a timestamp. If the timestamp of an incoming score isn't later than the timestamp already recorded for that user and line item, the platform is entitled to reject it. Canvas, among others, does, with a 409.

Where it bites: a retry. Your first attempt half-succeeded or timed out, the retry sends an identical payload with an identical timestamp, and the platform declines it. Or a batch job re-sends scores computed earlier with their original timestamps, and every one is older than what's already there.

Three rules:

  1. Generate the timestamp at send time, not at computation time.
  2. Make it strictly monotonic per user and line item.
  3. Treat a 409 as the platform already has this or newer, which is a success condition, not an error to retry into a loop.

Failure 3: gradingProgress

An AGS score has two progress fields, and they aren't decoration.

activityProgress is one of Initialized, Started, InProgress, Submitted, Completed.

gradingProgress is one of NotReady, Failed, Pending, PendingManual, FullyGraded.

A score is only shown as final when gradingProgress is FullyGraded. Send Pending and the platform can legitimately hold it. Teams who send Pending because the work isn't finished, or omit the fields and rely on a default, produce exactly the reported symptom: the request succeeded and nothing appeared.

If the work genuinely is partially complete, that's what activityProgress is for. Use both fields deliberately.

Failure 4: The grading period

The score arrives, in the right column, marked final, and doesn't count towards the grade the student sees.

Gradebooks organise line items into grading periods and into categories, and both affect weighting. A line item created without a grading period, or placed in one that's closed, sits outside the calculation. A line item in a category carrying zero weight in this teacher's setup contributes nothing.

This is where thin academicSessions data hurts, as flagged in OneRoster, honestly. If the district's feed gives you a single session for the whole year with no terms inside it, you have no way to place a line item in the right grading period, and your correct-looking scores quietly don't count.

Handle it explicitly: determine the grading period from the district's sessions where you have them, let an administrator configure the mapping where you don't, and surface which period a score was written to rather than leaving it implicit.

The design that holds

Make the score an object with a lifecycle, not an event. Store every attempt with its state (computed, sent, accepted, rejected, superseded) keyed on user and line item. That turns "did it land" into a query rather than a guess, and gives you the retry surface for free.

Make it idempotent. The same result computed twice produces one score. Key on the result, not the request.

Retry with real backoff, and cap it. Platforms rate-limit AGS aggressively. A district-wide re-send that hammers a platform is how you become a name their LMS administrator remembers.

Never let passback failure lose the work. The evidence lives in your system. Passback is a copy for the teacher's convenience. If a platform is down for six hours, students keep working and scores queue.

Show the teacher the state. The most valuable feature here is a small honest indicator: this score was sent at 09:14 and accepted; that one was rejected because the assignment was deleted in the LMS. A teacher who can see what happened doesn't file a ticket. A teacher looking at silence always does.

Build the support path once

When a teacher reports a missing grade, the questions are always the same:

  1. Which line item did we write to, and does it still exist in the LMS?
  2. What was the last response the platform gave us, verbatim, with a timestamp?
  3. What gradingProgress did we send?
  4. Which grading period is that line item in?
  5. Is this teacher looking at the section we wrote to? (Co-taught sections make this a real question, not a facetious one.)

A support view answering those five on one screen removes most of the back and forth. Without it, every one of these becomes an engineer reading logs.

A note on timing

Passback traffic isn't evenly distributed. It clusters at the end of a class period, at the end of a school day, and most sharply in the last week of a grading period, when teachers chase completion before grades close.

That last one deserves specific design. A queue that copes fine all term can back up for hours in the week grades are due, which is the single week when a delay is most visible and least forgivable. Rate-limit per platform, prioritise recent work over backfill, and know what your queue depth means in minutes rather than in messages.

Common questions

Why are grades not showing up in Canvas after passback?
Usually one of four causes: no line item is bound to the resource link, the score timestamp is not later than the existing one, gradingProgress was not set to FullyGraded, or the line item sits in the wrong grading period. All four return a success response.
What does gradingProgress FullyGraded mean in LTI AGS?
It tells the platform the score is final and can be displayed. Sending Pending or omitting the field lets the platform legitimately hold the score, which produces the common symptom of a successful request that never appears in the gradebook.
Why did my LTI AGS score return a 409?
Because the score's timestamp was not later than the timestamp already recorded for that user and line item. This usually happens on a retry that reuses the original timestamp. Generate timestamps at send time and treat a 409 as the platform already holding an equal or newer score.

Working on something that has to survive a district?