Scores and Results
Once a tool has a line item, it can write grades to it and read them back. Two request types: SCORE_POST to record a grade, and RESULTS_GET to read them.
Requires Assignment and Grades enabled on your account, plus GRADES_WRITE for posting and GRADES_READ for reading in the tool's permissions.
Recording a Score
{
"type": "SCORE_POST",
"parameters": {
"context": "2022CSEa5e6c431b91",
"clientId": "348080fn9du9b9ufvb92rfb9l",
"lineItemId": "412",
"score": {
"userId": "41",
"activityProgress": "Completed",
"gradingProgress": "FullyGraded",
"comment": "Great work!",
"scoreGiven": 99,
"scoreMaximum": 100
}
}
}
async function handleScore(decoded, res) {
const { context, lineItemId, score } = decoded.parameters
const lineItem = await db.lineItems.find(lineItemId)
if (!lineItem || lineItem.courseId !== context) {
return res.status(404).json({ error: 'Line item not found' })
}
// A score without scoreGiven is a progress update, not a grade.
if (score.gradingProgress === 'FullyGraded' && score.scoreGiven !== undefined) {
await db.grades.upsert({
lineItemId,
userId: score.userId,
score: score.scoreGiven,
maximum: score.scoreMaximum,
comment: score.comment
})
}
await db.activityProgress.upsert({
lineItemId,
userId: score.userId,
activityProgress: score.activityProgress,
gradingProgress: score.gradingProgress
})
return res.status(200).json({})
}
Return 200 with an empty JSON object.
Your status code is passed straight back to the tool. Reject a grade with a 4xx and the tool sees the rejection — many will retry later, which is usually what you want for a transient failure.
Scores Are Not Always Grades
scoreGiven is optional. A tool may post a score purely to report progress — "the learner opened this", "the learner submitted, grading is pending" — with no number attached. Recording those without touching the gradebook is what the two progress fields are for.
activityProgress — how far the learner has got:
| Value | Meaning |
|---|---|
Initialized | The activity exists but has not been started |
Started | The learner has begun |
InProgress | Actively working |
Submitted | Handed in, awaiting grading |
Completed | Finished |
gradingProgress — how far the tool has got with grading:
| Value | Meaning |
|---|---|
NotReady | Nothing to grade yet |
Pending | Queued for automatic grading |
PendingManual | Waiting for a human |
FullyGraded | Done — the score is final |
Failed | Grading failed |
Only treat a score as final when gradingProgress is FullyGraded. Writing Pending scores into the gradebook shows learners marks that are about to change.
When scoreGiven is present, scoreMaximum is present too — and it may differ from the line item's scoreMaximum. Store the pair, or scale as you save. Treating scoreGiven as a percentage is a common and expensive mistake.
Returning Results
{
"type": "RESULTS_GET",
"parameters": {
"context": "2022CSEa5e6c431b91",
"clientId": "348080fn9du9b9ufvb92rfb9l",
"lineItemId": "412",
"filters": { "userId": "14" }
}
}
async function handleResults(decoded, res) {
const { context, lineItemId, filters = {} } = decoded.parameters
const lineItem = await db.lineItems.find(lineItemId)
if (!lineItem || lineItem.courseId !== context) {
return res.status(404).json({ error: 'Line item not found' })
}
let grades = await db.grades.forLineItem(lineItemId)
if (filters.userId) {
grades = grades.filter(g => g.userId === filters.userId)
}
return res.status(200).json(grades.map(g => ({
id: `${lineItemId}-${g.userId}`,
userId: String(g.userId),
resultScore: g.score,
resultMaximum: g.maximum,
...(g.comment && { comment: g.comment })
})))
}
Always return an array, even for a single-user filter. id just has to be unique and stable — combining the line item and user IDs is fine.
| Field | Required | Meaning |
|---|---|---|
id | yes | Identifier for the result |
userId | yes | Must match the user value used on launches |
resultScore | no | The grade |
resultMaximum | no | What it was out of |
comment | no | Feedback text |
Grades a Teacher Changed
If a teacher overrides a tool-posted grade in your gradebook, return the override — RESULTS_GET should reflect what your gradebook actually holds, not what the tool last sent. Your LMS is the system of record.
Next Steps
- Memberships — the roster these grades belong to.
- Error handling — what to return when something goes wrong.
