Advanced Statement Structure: Voiding, Sub-Statements, Extensions, and Attachments¶
Summary¶
Covers result fields, context activities, voiding semantics, sub-statements, attachments, extensions, and reusable statement patterns. This chapter fits into the overall progression by building on prior concepts and preparing readers for the chapters that follow. After completing this chapter, students will be able to recognize, explain, and apply the concepts listed below in the context of xAPI-instrumented intelligent textbooks.
Concepts Covered¶
This chapter covers the following 16 concepts from the learning graph:
- Statement Voiding
- Context Activities
- Grouping Context Activity
- Parent Context Activity
- Category Context Activity
- Other Context Activity
- Result Score
- Result Success
- Result Completion
- Result Duration
- Result Response
- Extensions (xAPI)
- Attachment Object
- Sub-Statement
- Statement References
- xAPI Statement Patterns
Prerequisites¶
This chapter builds on concepts from:
Welcome to the Deep End of the Statement
Last chapter we built a basic, conformant statement and called it good. This chapter is where we take that same statement and ask harder questions: How did the learner do, exactly? Where in the course did this happen? What if the activity provider sent the wrong statement and needs a do-over? What if a single learning event is actually a story made of several events? Every interaction tells a story — and now we'll learn how to tell complicated ones.
Your New Superpower¶
By the end of this chapter, you'll be able to express precise pedagogical meaning in a single statement, and recover gracefully when something is wrong. Most teams spend their first three months in xAPI emitting flat statements that look correct and convey almost no useful analytics. The difference between an LRS full of "Lin experienced the chapter" and an LRS full of "Lin scored 0.92 on the quadratics quiz, which is part of Algebra 1, which is part of the 9th-grade math sequence, after a 47-second attempt, having previously failed once" is everything you're about to learn.
You'll also pick up the rarer skill of fixing past mistakes without erasing them — a property that makes xAPI auditable in ways most analytics pipelines aren't. We don't delete bad statements. We void them, on the record. The void is itself a statement. The trail stays intact.
Everything in this chapter slots into the optional parts of the statement we already built: result, context, attachments, plus the special-case statement shapes that reference or replace other statements. The required core (actor, verb, object) does not change. We're decorating the cake, not rebuilding the oven.
Result, in Detail¶
Chapter 2 introduced result as the field that answers "How did it go?" but didn't crack it open. The result object has five standard sub-fields, each addressing a different facet of how an activity ended. Before we look at a worked example, let's define each one in plain language.
score— a numeric performance measure. xAPI lets you express it as ascaledvalue (between -1 and 1, normalized), arawvalue (the actual points earned), and amin/maxrange. The scaled form is what dashboards usually graph because it doesn't depend on the test's point structure.success— a boolean that answers "did the learner pass the bar set by the activity?" This is independent of completion. A learner can complete a quiz and not pass it.completion— a boolean that answers "did the learner finish the activity?" Independent of success. A learner can fail the quiz but still finish it.duration— how long the activity took, encoded as an ISO 8601 duration string likePT47S(47 seconds) orPT3M12S(3 minutes, 12 seconds). ThePTprefix means "period of time" — yes, really.response— a free-form string the learner produced. Useful for fill-in-the-blank, short answers, code submissions, or any artifact you want preserved verbatim alongside the score.
Notice that success and completion are deliberately separate booleans. The xAPI authors learned from SCORM, where conflating these two led to years of confused gradebooks. Treating them as orthogonal axes — completion × success — produces four meaningful states:
| Completion | Success | Meaning |
|---|---|---|
true |
true |
Finished and passed |
true |
false |
Finished and failed |
false |
true |
Bailed early but met the bar (rare; usually a mastery exit) |
false |
false |
Bailed early without meeting the bar |
Here's how all five sub-fields fit together in a real result block. Lin attempted a 4-question quiz on factoring quadratics, spent 47 seconds, got 3 of 4 right, and submitted "x = -3, x = 5" as their final response:
"result": {
"score": {
"scaled": 0.75,
"raw": 3,
"min": 0,
"max": 4
},
"success": true,
"completion": true,
"duration": "PT47S",
"response": "x = -3, x = 5"
}
A few worked details to internalize. The scaled value equals raw / max only when min is 0; otherwise the spec normalizes against the full range, so a raw of 3 in a -2 to 4 range gives a scaled of \( (3 - (-2)) / (4 - (-2)) \), or roughly 0.83. The duration must be a valid ISO 8601 duration; PT47S parses, but 47s does not. And the LRS does not validate that the response makes sense for the activity — that semantic check is your job.
Diagram: Result Field Composition Explorer¶
Result Field Composition Explorer
Type: micro-sim
sim-id: result-field-composition-explorer
Library: p5.js
Status: Specified
Learning objective (Bloom — Applying): Given a learner scenario (e.g., "passed the quiz with 85%"), the student selects appropriate values for each result sub-field and sees the rendered JSON update in real time, reinforcing the orthogonality of completion and success.
Layout: 2/3 + 1/3 panel split with responsive design that re-flows on window resize. Left panel: input controls. Right panel: live-rendered JSON result object with syntax highlighting.
Controls (left panel):
- Slider: Raw score (0 – 10, default 7)
- Slider: Min score (-5 – 0, default 0)
- Slider: Max score (5 – 20, default 10)
- Computed display: Scaled score (auto-updates from raw/min/max)
- Toggle: success (true / false / unset)
- Toggle: completion (true / false / unset)
- Slider: duration in seconds (0 – 600, default 47)
- Text input: response (default
x = -3, x = 5) - Preset buttons: "Passed Cleanly", "Failed but Finished", "Bailed Early", "Mastery Exit"
Visual elements:
- Live JSON output with monospace font, color-coded keys/values
- A 2×2 grid showing the four completion×success states, with the current state highlighted
- Duration shown both as ISO 8601 string and human-readable form
Interaction: Every control change recomputes the JSON immediately and redraws the 2×2 grid. Preset buttons populate all controls at once for quick exploration.
Default canvas: 900×500px with media queries for narrower viewports.
Implementation: p5.js with a JSON renderer; no external dependencies beyond the p5.js CDN.
Context, in Detail¶
Where result answers "how did it go?", context answers "under what circumstances?". Chapter 2 introduced context and mentioned platform, language, and contextActivities. We'll spend most of this section on contextActivities because it's the structurally interesting part — and the source of most cross-platform analytics confusion when teams get it wrong.
A context activity is an activity that's associated with the statement's main object but is not the main object itself. The xAPI spec sorts these associated activities into four named buckets: parent, grouping, category, and other. Each bucket has a clear semantic meaning and each is an array, so a single statement can declare multiple parents, multiple groupings, and so on. Before we draw the relationship diagram, let's define each bucket in prose.
- Parent context activity — the activity that directly contains this object as a logical component. If the object is "Quiz on Quadratics," the parent might be "Algebra 1, Unit 5." The parent is the immediately enclosing scope.
- Grouping context activity — a broader collection the object belongs to. Where parent is the parent in a tree, grouping is membership in a set. The same quiz might be
grouping-ed under "All formative assessments in Algebra 1" — a horizontal collection rather than a containing scope. - Category context activity — declares conformance to a profile or vocabulary. This is how implementations advertise that they follow, say, the cmi5 profile or a custom vocabulary profile. Categories are the xAPI equivalent of "I'm using the 2026 ACME math vocabulary."
- Other context activity — anything related but doesn't fit the three above. Use sparingly; if you find yourself relying on
other, ask whether you should design a custom category instead.
These buckets matter because LRS query filters operate on them. Your dashboard team will eventually want to ask "show me every statement whose parent is algebra-1-unit-5," and the answer will be fast because the LRS indexes these buckets. Putting an activity in the wrong bucket isn't a syntax error — it's a slow-creeping analytics bug that won't surface until quarter three.
Diagram: Context Activity Buckets¶
Context Activity Buckets
Type: clickable-mermaid
sim-id: context-activity-buckets
Library: Mermaid
Status: Specified
Learning objective (Bloom — Understanding): Identify the role of each context activity bucket and explain how a single statement's main object can be related to multiple activities through different bucket relationships.
Diagram type: Mermaid flowchart, LR direction, with click handlers on every node opening an infobox containing the term's definition.
Structure:
- Center node: "Statement Object: Quadratics Quiz"
- Four labeled fan-outs to:
- Parent: "Algebra 1, Unit 5" (edge label
directly contains) - Grouping: "Formative Assessments — Q3 2026" (edge label
is a member of) - Category: "ACME Math Profile v2" (edge label
conforms to) - Other: "Pre-class warm-up flow" (edge label
associated with)
- Parent: "Algebra 1, Unit 5" (edge label
Mermaid config (project standard):
flowchart: {
useMaxWidth: true, htmlLabels: true, curve: 'basis',
nodeSpacing: 12, rankSpacing: 60, padding: 4
}
Click behavior: Each node opens a side-panel infobox showing the term, its definition (drawn from the project glossary when available), and a one-line example. The center node's infobox explains the statement's main object.
Default canvas: 2/3 width for the diagram, 1/3 for the side panel. Responsive — re-flows to stacked layout below 700px.
Implementation: Mermaid with securityLevel: 'loose' to enable click handlers, plus a small JavaScript layer that renders the side panel from a data.json keyed by node id.
Here's a context block that exercises three of the four buckets — every textbook quiz statement should look something like this:
"context": {
"platform": "Intelligent Algebra Textbook",
"language": "en-US",
"contextActivities": {
"parent": [{
"id": "http://textbook.example.org/algebra-1/unit-5",
"objectType": "Activity"
}],
"grouping": [{
"id": "http://textbook.example.org/assessments/formative-q3-2026",
"objectType": "Activity"
}],
"category": [{
"id": "http://textbook.example.org/profiles/acme-math-v2",
"objectType": "Activity",
"definition": {
"type": "http://adlnet.gov/expapi/activities/profile"
}
}]
}
}
Notice the category entry has a definition.type of profile — that type IRI is the convention for declaring that the category bucket is signaling profile conformance, not just a related activity. Different bucket, different convention.
Xavi's Tip — Pick a Bucket and Stick With It
Inconsistent bucket choice across a textbook is the number-one thing that makes cross-platform analytics painful later. Decide early: chapters are parent, the textbook itself is the deepest parent, vocabulary profiles are category. Write it down. Audit it. Future-you will send present-you a thank-you note.
Extensions — The Sanctioned Escape Hatch¶
xAPI gives you a sanctioned way to attach arbitrary data to a statement: extensions. An extension is a key-value pair where the key is an IRI you own and the value is any JSON. Extensions can appear inside result, inside context, or inside an activity's definition. They exist because no fixed schema can predict every kind of data a learning experience might want to record, and the alternative — overloading existing fields — is worse.
Before we use one, the rules. The key must be a full IRI in a namespace you control. temperature is not a valid extension key. https://textbook.example.org/extensions/scratchpad-keystrokes is. The value can be any JSON — string, number, object, array, even a deeply nested structure — but the LRS will treat the whole thing as opaque. It will store it, return it on query, and not interpret it.
Here's a result block with an extension that records the keystroke trail a learner produced inside a code-editor MicroSim. The keystrokes array is your private payload; the LRS round-trips it without judgment:
"result": {
"completion": true,
"success": true,
"score": { "scaled": 1.0 },
"extensions": {
"https://textbook.example.org/extensions/v1/code-editor-trace": {
"keystrokes": [
{"t": 0.00, "key": "f"},
{"t": 0.12, "key": "u"},
{"t": 0.24, "key": "n"}
],
"errors-before-success": 2,
"language": "python"
}
}
}
When designing extensions, two pieces of advice from teams who have lived through five years of an xAPI deployment. First, version your namespace from day one — extensions/v1/code-editor-trace not extensions/code-editor-trace. The day you need to change the schema, you'll thank yourself. Second, document every extension key in a single place inside your codebase so future maintainers can find them. Extensions buried in twenty different statement-builder functions become orphan data within months.
Attachments — When You Need Real Bytes¶
Sometimes a learning experience produces an artifact that doesn't compress nicely into JSON: an essay PDF, a screenshot of a final design, a recorded audio answer. The attachment object is xAPI's mechanism for shipping that artifact alongside the statement. An attachment isn't a URL pointing at a file somewhere — it's an actual binary blob delivered as part of a multipart HTTP POST, with a metadata header describing it.
Attachments are powerful and structurally annoying. Most LRS clients use application/json POSTs for plain statements; the moment you add an attachment, the request becomes multipart/mixed, the parser needs a boundary, and many off-the-shelf HTTP libraries get fussy. Save attachments for cases where the artifact genuinely needs to live with the statement and can't reasonably live behind a URL. Here's the metadata block that describes an attached PDF essay:
"attachments": [{
"usageType": "http://adlnet.gov/expapi/attachments/signature",
"display": { "en-US": "Final essay submission" },
"description": { "en-US": "PDF of the learner's final essay, signed digitally." },
"contentType": "application/pdf",
"length": 84231,
"sha2": "672fa5fa658017f1b72d65036f13379c6ab05d4ab3b6664908d8acf0b6a0c634"
}]
The sha2 field is what links the metadata to the actual binary. The binary is shipped in a separate part of the multipart POST, identified by the same SHA-2 hash. The LRS verifies the hash on receipt; if the bytes don't match the hash, the LRS rejects the whole statement.
Common Pitfall — Attachments and Privacy
Attachments often contain identifiable learner work — handwriting, voice, faces in screenshots. They flow into the same LRS as the rest of your statements but typically aren't covered by the same retention controls unless you've built that explicitly. Decide your attachment policy before you start collecting them, not after.
Statement References — Pointing at Other Statements¶
A statement reference is a special kind of statement object that points to another statement by its UUID. Where a normal statement's object is an activity (like a quiz), a statement-reference statement's object is another statement. This is how xAPI expresses statement-about-statements relationships — the most important of which is voiding (next section), but there are other uses too: marking a statement as graded, marking a statement as appealed, marking a statement as the canonical one of three near-duplicates.
The shape is simple. The objectType is StatementRef and the only required field is the target statement's id. Here a teacher records a graded statement that points at Lin's earlier quiz attempt by UUID:
{
"actor": { "mbox": "mailto:teacher@example.edu" },
"verb": {
"id": "http://textbook.example.org/verbs/graded",
"display": { "en-US": "graded" }
},
"object": {
"objectType": "StatementRef",
"id": "fd41c918-b88b-4b20-a0a5-a4c32391aaa0"
},
"result": { "score": { "raw": 4 } }
}
The original is not modified — statements are immutable in a conformant LRS. The graded statement is a new record that talks about the original.
Voiding — Cancellation Without Deletion¶
A conformant LRS never deletes a statement. If a statement was sent in error — wrong learner, wrong score, wrong activity — the way to retract it is to void it. Voiding is implemented as a statement reference with a special verb: http://adlnet.gov/expapi/verbs/voided. The voiding statement points at the bad statement's UUID and says, in effect, "this previous statement should no longer be considered."
The original statement is still there. It still has its UUID. It still appears if you query for it directly. But it will not be returned in a normal /statements query unless you explicitly ask for voided statements (voided=true filter). Voiding is non-destructive cancellation — the audit trail stays intact, but the analytics layer behaves as if the statement isn't there.
Here's a voiding statement that retracts Lin's earlier quiz statement, perhaps because the activity provider sent the wrong score:
{
"actor": {
"objectType": "Agent",
"account": {
"homePage": "https://lrs.example.org",
"name": "intelligent-algebra-client"
}
},
"verb": {
"id": "http://adlnet.gov/expapi/verbs/voided",
"display": { "en-US": "voided" }
},
"object": {
"objectType": "StatementRef",
"id": "fd41c918-b88b-4b20-a0a5-a4c32391aaa0"
}
}
Voiding has subtle rules. Only the original authority of a statement (or an LRS admin acting on its behalf) should void it — anyone voiding statements they didn't author is a security smell. Voided statements cannot themselves be voided ("un-voiding" is not a thing in xAPI; if you need to reinstate a voided statement, you re-emit it as a new statement with a new UUID). And the LRS may or may not let an Activity Provider modify the original — that depends on whether the LRS implements mutable or immutable storage. In an immutable LRS, voiding is the only path to retraction.
Diagram: Voiding Lifecycle Flow¶
Voiding Lifecycle Flow
Type: workflow-diagram
sim-id: voiding-lifecycle-flow
Library: Mermaid
Status: Specified
Learning objective (Bloom — Analyzing): Trace the lifecycle of a statement that gets voided, identifying which records persist in the LRS and which are filtered from default queries.
Diagram type: Mermaid flowchart (LR direction) representing the temporal sequence as a left-to-right pipeline. Click handlers open an infobox for every node.
Nodes (left to right):
AP emits original statement(uuid=fd41…)LRS stores originalDefault /statements query — original visibleAP discovers errorAP emits voiding statement(StatementRef → fd41…, verb=voided)LRS stores voiding statement and flags fd41…Default /statements query — original filtered out, voiding statement visiblevoided=true query — both visible
Edges: Solid arrows for the main flow; a dashed arrow loops from node 7 back to node 8 to indicate that operators can still inspect voided records on demand.
Mermaid config: project standard (nodeSpacing: 12, rankSpacing: 60, padding: 4, useMaxWidth: true, securityLevel: 'loose').
Click behavior: Each node opens a side-panel infobox keyed off data.json. Glossary terms (StatementRef, voided, authority, audit trail) link to their full glossary definitions.
Default canvas: 2/3 width diagram + 1/3 side panel. Stacks vertically below 700px.
Implementation: Mermaid flowchart with click directives bound to a side panel. The side panel is a vanilla JS component that swaps content based on the clicked node id.
Sub-Statements — Statements Inside Statements¶
A sub-statement is a statement that appears as the object of another statement. Where a statement reference points at an existing statement by UUID, a sub-statement embeds a full statement object inline. Sub-statements are how xAPI expresses statements that describe what someone said about a learning event, rather than the learning event itself.
This is one of the harder parts of xAPI to internalize. Read it twice and don't worry if it doesn't click immediately.
Concretely: Lin attempts a quiz. That's one statement, with Lin as the actor. A teacher then watches Lin attempt the quiz and observes "Lin showed strong reasoning skills during the attempt." The teacher's observation is its own statement — actor: teacher, verb: observed — but the object of the teacher's observation is the entire act of Lin attempting the quiz. That entire act, including its actor and verb and object, is embedded as a sub-statement.
{
"actor": { "mbox": "mailto:teacher@example.edu" },
"verb": {
"id": "http://textbook.example.org/verbs/observed",
"display": { "en-US": "observed" }
},
"object": {
"objectType": "SubStatement",
"actor": { "mbox": "mailto:lin@example.edu" },
"verb": {
"id": "http://adlnet.gov/expapi/verbs/attempted",
"display": { "en-US": "attempted" }
},
"object": {
"objectType": "Activity",
"id": "http://textbook.example.org/chapters/quadratics/quiz"
}
},
"result": {
"response": "Strong algebraic reasoning. Tried symbolic factoring before guess-and-check."
}
}
Sub-statements have two structural rules. They cannot themselves contain sub-statements (no recursion — a sub-statement object's object must be a regular activity or a statement reference). And they cannot have an id, stored, version, or authority field — those metadata fields belong only to the outer statement, because the sub-statement is embedded and not independently stored as a record. It only exists as part of its parent.
When to use a sub-statement vs. a statement reference is the question that trips up most teams. After the prose above, this comparison should now read as a summary, not an introduction:
| You want to say... | Use |
|---|---|
| "X did this, and here's exactly what they did" | Sub-statement |
| "X did this thing that I already wrote down separately" | Statement reference |
| "I'm grading / annotating / appealing a previous record" | Statement reference |
| "I observed / planned / predicted a hypothetical event" | Sub-statement |
This Part Is Tricky — That's Normal
Sub-statements vs. statement references is the part of xAPI that needs the most rereads. If your first instinct after this section is "I think I get it, but I'd want to look at three real examples before betting money on which one to use," that's the right instinct. Bookmark this section — you'll come back.
Composing the Pieces — xAPI Statement Patterns¶
An xAPI statement pattern is a reusable template for emitting statements that describe a recurring kind of learning event. Patterns are not part of the formal spec — they're a community practice. Every mature xAPI implementation eventually develops a small library of patterns that the team agrees to use whenever a particular event occurs. This is how teams achieve cross-platform analytics consistency without manually reviewing every emit site.
A pattern combines decisions about: which verb to use, which activity type, which context activity buckets to populate, which result fields are mandatory, which extensions are allowed, and which optional fields the team agrees to always include even though the spec doesn't require them. Once written down, a pattern lives in your codebase as a function (often a TypeScript builder) that other code calls. The pattern enforces consistency by being the only path to a particular kind of statement.
Five patterns that show up in nearly every intelligent textbook deployment:
- Page-Read pattern — verb
experienced, activity typechapterorpage, parent context activity = textbook root, no result. - Quiz-Submission pattern — verb
attempted, thenpassed/failed, withresultcontainingscore,success,completion,duration, andresponse. Parent = chapter, grouping = assessment cohort. - MicroSim-Interaction pattern — verb
interacted, custom activity type, result extension for the interaction trace, parent = chapter. - Adaptive-Branching pattern — verb
progressed, with a context extension for the branch decision, parent = adaptive activity root. - Voiding pattern — verb
voided, object =StatementRef, no result, no context activities. Strict by design.
The payoff of writing patterns down is that your dashboard team gets to write one query per pattern, not one query per emit site. That's the leap from "we're emitting xAPI" to "we have working learning analytics."
Diagram: Statement Pattern Composer¶
Statement Pattern Composer
Type: micro-sim
sim-id: statement-pattern-composer
Library: p5.js
Status: Specified
Learning objective (Bloom — Creating): Compose a complete xAPI statement by selecting a pattern, filling in pattern-specific slots, and observing the rendered statement to learn how patterns enforce consistency across emit sites.
Layout: 2/3 (left) form + 1/3 (right) live JSON preview, responsive, re-flowing on resize.
Controls:
- Dropdown: pattern type (Page-Read, Quiz-Submission, MicroSim-Interaction, Adaptive-Branching, Voiding)
- Dynamic form fields that change based on the selected pattern (e.g., Quiz-Submission shows score sliders, success toggle, duration; Page-Read shows only chapter selector)
- Static fields visible for all patterns: actor (text), parent activity (dropdown of fake textbook chapters)
Visual elements:
- Live JSON preview of the resulting statement, syntax-highlighted
- A "Pattern Slot Map" that highlights which fields the pattern enforces, which are optional, and which are forbidden — color-coded green / yellow / red
- Validation badge ("Pattern Valid" / "Missing Required Slot") that updates live
Interaction: Selecting a different pattern re-renders the form and the slot map. Filling in slots re-renders the JSON. Invalid combinations are flagged but not blocked — this is a learning tool, not a validator.
Default canvas: 1000×550px, responsive.
Implementation: p5.js for the canvas drawing of the slot map; HTML form controls overlaid for accessible inputs. JSON preview rendered into a styled <pre> element.
What You Just Leveled Up¶
Walk through this checklist. If any of these are unfamiliar, that's where to reread before moving to Chapter 4.
- You can populate a
resultobject that distinguishes completion from success, formatsdurationas ISO 8601, and usesscaledcorrectly whenminis non-zero. - You can slot context activities into the right bucket (
parent,grouping,category,other) and explain why each bucket exists. - You can attach arbitrary structured data with
extensions, using IRIs you own and versioning your namespace. - You can ship a binary artifact with an
attachmentand you know why you'll usually wish you hadn't. - You can void a bad statement without deleting it, preserving the audit trail.
- You can choose between a statement reference (point at an existing statement) and a sub-statement (embed a hypothetical or observed statement) for the right reasons.
- You can articulate why your team needs a small library of statement patterns and what would be in it.
That's a lot of vocabulary in one chapter. The good news: you'll spend the rest of the book using these tools, not learning new shapes of the same statement. Chapter 4 turns to the verbs themselves — what makes a good verb vocabulary, how to design one for your textbook, and how to avoid the classic verb-design mistakes that turn a sharp analytics setup into a fuzzy one.
Eight Tentacles Up
You just took the longest, densest section of the xAPI spec and turned it into working knowledge. From here, every chapter builds outward — verbs, identities, LRS architecture, instrumentation, networks. The hard schema work is behind you. The data never lies — but the schema might, and now you know how to make yours tell the truth.
<<<<<<< HEAD¶
References¶
d2ecc9b (iframe updates) See Annotated References