What is a Learning Record Store (LRS), and do you need one?

What is a learning record store? A database that speaks xAPI: it receives, stores, and serves statements. Contents, queries, forwarding, when to skip it.

What an LRS is

What is a learning record store? Mechanically, it is a web service that implements the server side of the xAPI specification: a handful of HTTP resources under one base URL that accept JSON statements, keep them, and hand them back on request. The xAPI spec defines it as the system responsible for "receiving, storing, and providing access to" learning records. It is not an LMS: no course catalogue, no enrolment, no launch button, and no reports beyond a filtered list. What it has is a strict contract — validate every incoming statement, never alter one once stored, answer the same queries in the same shape as every other conformant store — and that contract is what lets content from one vendor, a store from another, and a dashboard from a third agree on what happened.

If statement, actor, and verb are new words, What is xAPI covers the protocol; this article is about the store.

WRITES IN xAPI content (any URL) POST /statements cmi5 AU, launched by LMS POST /statements App / simulator / HR system POST /statements LRS statements (immutable) state · profiles xAPI 1.0.3 / 2.0 READS OUT Reporting / dashboard GET /statements?… LMS rollup (cmi5 moveOn) GET ?registration=… Another LRS (forwarding) POST /statements Every arrow is plain HTTPS with JSON. Nothing on the left needs to know anything on the right exists.
An LRS decouples the things that produce learning data from the things that consume it.

What it stores

Four kinds of thing, each behind its own resource under the base URL — the "endpoint" a launch or config file refers to, such as https://lrs.example.org/xapi/.

ResourcePathHoldsSCORM counterpart
Statements/statementsImmutable records of what happened: actor, verb, object, result, context, timestampThe values written with SetValue — score, status, session time — but as events, not a single overwritten record
State/activities/stateAny document keyed by activity + agent (+ optional registration). Bookmarks and in-progress answers live herecmi.suspend_data and cmi.location, with no 4 KB / 64 KB cap in the spec
Activity Profile/activities/profileDocuments about an activity itself, independent of learnerNothing directly; closest is manifest metadata
Agent Profile/agents/profileDocuments about a learner, independent of activity (cmi5 keeps cmi5LearnerPreferences here)cmi.learner_preference.*
About/aboutWhich xAPI versions the store acceptsNothing

The statement rules are where the store earns its keep. On every POST it checks that the actor has exactly one identifier, that verb and activity ids are IRIs, that result.score.scaled is between −1 and 1, that duration is ISO 8601, and that the X-Experience-API-Version header is present; anything malformed gets a 400 and nothing is stored. Once stored, a statement cannot change: a PUT to /statements?statementId=… with an existing id and different content is refused with 409 Conflict. To retract one you send a new statement whose verb is http://adlnet.gov/expapi/verbs/voided and whose object is a StatementRef to the original, which stays on disk and is excluded from normal queries.

The State resource deserves a concrete example, because it is what makes resume work. A course saving a bookmark for learner dana in …/fire-safety/module-1 does this:

PUT /xapi/activities/state
    ?activityId=https%3A%2F%2Fexample.org%2Fcourses%2Ffire-safety%2Fmodule-1
    &agent=%7B%22account%22%3A%7B%22homePage%22%3A%22https%3A%2F%2Flms.example.org%22%2C%22name%22%3A%22dana%22%7D%7D
    &registration=6c1e8f0a-2d4b-4b7e-9a3f-0f5b1c2d3e4f
    &stateId=bookmark
Content-Type: application/json
X-Experience-API-Version: 1.0.3

{ "page": 14, "answers": { "q3": "b", "q7": "d" }, "videoPos": 212.4 }

HTTP/1.1 204 No Content

On the next launch the course issues the same request as a GET and receives the document back. Compare the SCORM failure mode where a bookmark string silently truncates at 4,096 characters: here the document is whatever JSON you wrote, and a rejection comes back as an error code.

Querying and reporting

Reading is a GET on /statements. The specification defines exactly these filters: statementId, voidedStatementId, agent, verb, activity, registration, related_activities, related_agents, since, until, limit, format, attachments, and ascending. "Everything Dana did in the fire-safety course since Monday" is one request:

GET /xapi/statements
    ?agent=%7B%22mbox%22%3A%22mailto%3Adana%40example.org%22%7D
    &activity=https%3A%2F%2Fexample.org%2Fcourses%2Ffire-safety
    &related_activities=true
    &since=2026-08-24T00:00:00Z
    &limit=100
X-Experience-API-Version: 1.0.3

HTTP/1.1 200 OK
{
  "statements": [ … up to 100 statements, newest first … ],
  "more": "/xapi/statements/more/7d2c1f…"
}

The more URL is the next page; an empty string means you have everything. related_activities=true matches statements where the course id appears anywhere in context.contextActivities, not only as the object — which is how you get statements about module 1 when you asked about the course.

Notice what is missing: no filter on score, no group-by, no count, no "learners who have not completed". The xAPI query interface is a filtered dump, deliberately, so that every store is interchangeable and analysis happens in whatever reads the dump. "An LRS with reporting" is therefore two products in one box — the conformant store, and a proprietary analytics layer that indexes statements into something you can chart — and you should evaluate them separately. The store's job is defined by ADL's open-source LRS conformance test suite, which anyone can run against any endpoint; the reporting layer's job is defined by nobody but you.

The identifiers are the join keys. Every report is a query on actor ids and activity ids. If your LMS regenerates account names on migration, or your authoring tool mints a new activity id per publish, the store fills with statements that cannot be related to each other. Fix the ids before the dashboard; LMS migration is the usual moment this bites.

Forwarding between record stores

Because a statement is a self-contained JSON document with a UUID, one store can send it to another with an ordinary POST /statements, and the receiving store keeps the same id. That is all "statement forwarding" is. The specification does not define it — most LRS products implement it — but the specification is what makes it work: an immutable record with a globally unique id can be copied anywhere without ambiguity, and a store that receives a statement it already holds must not store a second copy.

Three shapes come up in practice:

  • Vendor to customer. A training provider runs content against its own store and forwards statements about each customer's learners to that customer's store, filtered by actor homePage.
  • Department to central. Business units run their own stores and forward everything to one enterprise store for cross-unit reporting.
  • Old to new. A migration: page through GET /statements?since=… on the old store and POST the pages to the new one. Because ids and timestamps travel with the statements, history survives intact — something SCORM records, which live inside one LMS's database schema, generally do not.

Check two things first: that the source forwards voiding statements too, or the destination keeps records the source retracted; and that the destination's authority handling is what you expect: the specification says a store SHOULD overwrite authority with the credential that posted the statement and MAY keep the submitted one only under "a strong trust relationship", so by default the forwarded copy names the forwarding store as its authority, not the original content. If provenance matters, keep the original store.

When you need one

The answer follows from which standard your content uses, and it is more clear-cut than vendors make it.

What does the content report through? SCORM only API / API_1484_11 SCORM + xAPI side statements cmi5 cmi5.xml + statements Plain xAPI no package No LRS The LMS stores cmi.* values itself LRS for the extras Completion still comes from the LMS record LRS required Spec: the LMS MUST implement one LRS required Nothing else can receive the statements
Whether you need a store is decided by the standard, not by how much data you would like.
  • SCORM 1.2 or 2004 only: no. The content talks to the LMS's JavaScript API and the LMS stores the cmi.* values in its own database; there is nothing for an LRS to receive.
  • cmi5: yes, and the specification puts the obligation on the LMS — "the LMS MUST implement an LRS as defined in xAPI". A platform that claims cmi5 support has one, embedded or connected. Your question is whether you can read it, because your per-interaction statements land there too.
  • Plain xAPI: yes, unavoidably. The statements have to go somewhere, and the LRS is the only thing defined to receive them.
  • SCORM for the LMS plus xAPI statements on the side: yes, but only for the extra data. Completion and score still come from the SCORM record. This is the common first step because it changes nothing about the import the customer already trusts — the trade-offs are in SCORM vs cmi5 vs xAPI.

Built-in vs external

Once you need one there are three ways to get it, and the choice is less about features than about who owns the data and the identifiers.

Built into the LMSHosted LRS productSelf-hosted open source
Set-upNone; the endpoint exists alreadySign up, create a credential, note the endpointDeploy, back up, patch, monitor
Who can write to itUsually only that LMS's launchesAnything you give a key toAnything you give a key to
Reading it backOften only through the LMS's own reports; a raw GET /statements may not be exposedFull xAPI query plus the vendor's reportingFull xAPI query; reporting is yours to build or bolt on
Actor identifiersThe LMS's user idsWhatever each source sends — you must align themSame
LeavingExport depends on the vendorForward or page out with sinceIt is your database

Built-in is the right answer when cmi5 completion in one LMS is all you need. The moment a second system — a simulator, a mobile app, another LMS — must write statements about the same learners, an external store becomes the neutral ground where identifiers are reconciled and where no one platform's migration deletes the history. ADL publishes an open-source reference LRS alongside its conformance test suite, and other open-source stores such as Yet Analytics' SQL LRS and TRAX LRS exist if you would rather run one than rent one. Whichever you choose, run the conformance suite against it before you trust it.

Where SCORM Central fits

SCORM Central converts files you already have — PDF, PowerPoint, Word, or video — into SCORM 1.2, SCORM 2004, cmi5, or xAPI, and shows you the manifest, the full runtime log, and a conformance report on every build, so you can see exactly what a package will report before an LMS does. See how it works, or start with one file.

Keep reading

How-to4 min Convert SCORM to video or PowerPoint? Can you convert SCORM to MP4, HTML5 or PowerPoint? What is inside a package, the routes that work, and what you lose with each one. Explainer4 min Does your LMS support xAPI? How to check What xAPI LMS support really means: launching xAPI content, a built-in LRS, or forwarding statements. The questions to ask and a quick test to run. Explainer4 min LMS vs LRS: what is the difference? LMS vs LRS: an LMS runs courses and learners; an LRS stores xAPI statements. Why SCORM does not use an LRS, and when you need both.

Start with one file.

Upload something you already have and see it as a package, with the conformance report and the runtime log alongside it.

→ upload  handbook.pdf
  parsing … 14 sections
  building manifest …
  validating … 0 errors, 2 notes
✓ handbook-scorm2004.zip
✓ preview link ready