xAPI is a great way to collect data on how consumers interact with your content. Not only that; xAPI also lets you use that data to control how content is presented. In this article, I’ll show you how to do that for whatever purpose you require (for example, making your learning content adaptive).

You can collect data on video consumption, quiz answers, page views, just about anything you can imagine. And not only do you have control over what events or interactions you track, you can also decide the granularity of that data. For example, you can send statements that say that someone looked at a page. You could send statements that a specific user looked at the page. You could send statements that a specific user clicked a specific button at a specific time on a specific page viewed in a specific browser from a specific part of the country. You have almost complete control!

But what good is data if you can’t use it? Certainly, all of the major learning record stores (LRSs) provide ways of visualizing and reporting on statements that have been sent to the LRS. That’s barely the promise of xAPI, though.

Consider the following example. You have two classes: Forklift Driving 101 and Forklift Driving 102, with 102 building directly on the progress and lessons from 101. When 102 starts, you might want to review certain lessons from Forklift Driving 101. But which activities need reviewing? Do you review all of them? Or just the ones with which your student had particular problems performing, previously?

It’s a classic example of adaptive learning content. The second class is automatically tailored to your student’s needs. But how do you get that data out of the LRS for the content to decide which lessons need review? By sending xAPI queries, of course!

xAPI queries are sent very similarly to statements (mostly… kind of… we’ll get to that). But instead of recording data, queries return data. And the good news is that they are pretty easy to build and send. The bad news is, there are a few rules about them that you need to know. These limit how you can ask the LRS for data, but they are vital to understand because you’ll want to make sure you build your statements in a way that allows you to get the data back out!

But I’m not a programmer or developer!

Good news! We’re not going to look at much code in this entry of the series! Instead, we’ll focus on how you can query the data, some of the limitations on what queries you can send, what the data will look like when you get it back, and how that information should guide the way you build your statements in the first place. If you want to make the most of xAPI, you need to understand these concepts. The two code samples that are included in this article are pretty simple, and they are easy to follow.

Getting started

Let’s take a simple statement I constructed using the ADL xAPI Lab Statement Builder:

{
    "actor": {
        "mbox": "mailto:aa_altieri@outlook.com",
        "name": "anthony altieri",
        "objectType": "Agent"
    },
    "verb": {
        "id": "http://adlnet.gov/expapi/verbs/answered",
        "display": {
            "en-US": "answered"
        }
    },
    "object": {
        "id": "http://adlnet.gov/expapi/activities/example",
        "definition": {
            "name": { en-US": "Example Activity" },
            "description": { “en-US": "Example activity description" }
        },
        "objectType": "Activity"
    },
    "result": {
        "score": {
            "scaled": 1,
            "raw": 100,
            "min": 0,
            "max": 100
        },
        "success": true,
        "completion": true,
        "response": "This is my answer"
    }
}  

It’s a pretty simple statement that says that I answered a question, I got it right, and my response to the question was “This is my answer.” Fairly basic stuff. Now, when I send this to an LRS, the LRS is going to store that statement and add a few bits for housekeeping and bookkeeping use later on, resulting in the statement looking like this:

{
	"verb": {
		"id": "http://adlnet.gov/expapi/verbs/answered",
		"display": { "en-US": "answered" }
	},
	"version": "1.0.0",
	"timestamp": "2017-07-23T00:28:31.551930+00:00",
	"object": {
		"definition": {
			"name": { "en-US": "Example Activity" },
			"description": { "en-US": "Example activity description" }
		},
		"id": "http://adlnet.gov/expapi/activities/example",
		"objectType": "Activity"
	},
	"actor": {
		"mbox": "mailto:aa_altieri@outlook.com",
		"name": "anthony altieri",
		"objectType": "Agent"
	},
	"stored": "2017-07-23T00:28:31.551930+00:00",
	"result": {
		"completion": true,
		"score": {
			"raw": 100,
			"max": 100,
			"scaled": 1,
			"min": 0
		},
		"response": "This is my answer",
		"success": true
	},
	"id": "33d6384b-973d-4e0d-bff4-75e82e135a02",
	"authority": {
		"mbox": "mailto:techteam+xapi-tools@adlnet.gov",
		"name": "xapi-tools",
		"objectType": "Agent"
	}
}

When you query the LRS, some data may be shifted around from the statement you originally sent. You’ll also notice that some information has been added that can be very important and helpful. I’ve summarized this in Table 1.

Table 1: Important and helpful LRS-added information

version (Line 6)

The version of xAPI I’m using (1.0, in this case).

timestamp (Line 7)

This is actually added by the xAPI wrapper and records the time that the statement was created.

stored (Line 21)

This is the time when the LRS recorded the statement. This could be quite a bit different from when the statement was created—for example, when using offline players.

id (Line 33)

This is the unique string assigned by the LRS that identifies this statement.

authority (Lines 34 to 38)

This object defines whose authority was used to send this statement. I used basic HTML security for this statement, using a standard user ID and password.

So now we know what data we sent, and we know what it looks like once the LRS has it. So, how do we get that data back?

Gimme! Gimme! Gimme!

The easiest way to get data out of an LRS is, of course, to use the built-in analytics provided by the LRS itself. Those are great for reporting en masse. But you may not be able to build the exact report you want. Also, as mentioned above, if you want to create an adaptive course, you’ll need to send a query directly from your activity to the LRS in order to get the data you need.

But it’s not as simple as asking the LRS, “Please tell me what Anthony did since yesterday.” Well… actually, OK, sometimes it is. Some queries are very simple. But others can be much more difficult. For example, what if you want to ask, “Who got question four correct?” That one is a little tricky. It turns out that there are some rules for what you can and cannot query directly. Luckily, those rules are fairly simple for most use cases.

You can query the following directly.

  • id: You can ask for a specific statement by the id (always in lower case) assigned by the LRS.
  • since: You can ask for statements that were recorded after a certain time. This uses the ISO 8601 time format, for example: 2017-07-19T23:20:50.52Z. Note that this looks at when the LRS recorded the statement, not when it was created. Again, this can be important if you are using an offline player.
  • until: Same as above, but this time looking for statements recorded before a certain time. This uses the ISO 8601 time format, for example: 2017-07-19T23:20:50.52Z. Same note applies, too.
  • verb: You can ask for any statement containing a certain verb id. Looking at the example above, that would be: 'http://adlnet.gov/expapi/verbs/answered'
  • activity: You can ask for statements referencing a certain object id where that object is listed as an activity. Looking at the example above, that would be: 'http://adlnet.gov/expapi/activities/example'
  • agent: Like the above, this is strictly speaking not querying by actor, but any statement that has a given agent defined in the statement. And unlike the above, this must always be sent as JSON object. Looking at the example above, that would be: '{"mbox":"mailto:aa_altieri@outlook.com"}'
  • limit: You can limit the number of statements returned. If you list “0”, the LRS will return the maximum number of statements allowed.
  • Ascending: This will return the statements in chronological order starting with the statement with the oldest stored date.

Each of these is optional. In fact, you don’t have to send any parameters at all. The LRS will simply return all statements up to the maximum number the LRS is configured to send by default. So what does this all mean?

The good

Well, it means we can send some pretty useful queries. Using the ADL xAPI wrapper, we could query for the above statement using any combination of the parameters listed in this example:

var myparams = ADL.XAPIWrapper.searchParams()
var d = new Date("July 19, 2017 12:00:00");
myparams['since'] = d.toISOString();
myparams['verb'] = 'http://adlnet.gov/expapi/verbs/answered';
myparams['activity'] = 'http://adlnet.gov/expapi/activities/example';
myparams['agent'] = '{"mbox": "mailto:aa_altieri@outlook.com"}';

//send the query
var ret = ADL.XAPIWrapper.getStatements(myparams);

As you can see, this code is actually even easier than building and sending statements! There are a couple things to look out for, though. Notice how I had to reformat the time and date on line 2. Again, timestamps in xAPI are in the ISO 8601 format. Also, notice that I had to use the full object (as a string) to describe the agent. I can display the resulting statements on a web page like this:

var txt = " ";
if (ret) {
	for (i = 0; i < ret.statements.length; i++) {
		var stmt = 	i + ".   " +
					ret.statements[i].actor.mbox + "         " +
					ret.statements[i].verb.id + "         " +
					ret.statements[i].object.id + "         " +
					ret.statements[i].timestamp + "
";	
		txt += stmt;
		}
	document.getElementById("results").innerHTML = txt;
	
}
This loop will walk through each of the returned statements, one at a time, format the data I want, and list them in the

“Results” on the page. In this case, I’m listing the actor’s email address, the verb and object IDs, and the ISO timestamp from when the statement was recorded by the LRS.

The bad

Since you may use each of those parameters only once per query, you may run into some statements you can’t query directly. For example, let’s say I send the statement “Anthony oriented Craig.” In this case, we have two agents: Anthony and Craig. But I can only query for one. So I can query for all statements including Anthony, or those including Craig. I can’t query for only those including both. Some parts of the statement you can’t query at all—for example, the results section. You can’t query for those statements where someone gave a correct answer. And you can’t query for those who gave a specific incorrect answer. You can only query those who answered. So straight away, there are some limits to filtering what you can request of the LRS. But there are ways around that. And that leads to…

The ugly

Sometimes, you need to see what the most popular incorrect answer was. Or you need to see when Anthony welcomed Craig to his new position at Widget Co. And you can, but it takes a little extra work. In the case of finding out who gave a specific answer, or who gave incorrect answers, you’d have to query for all statements where someone answered the question. Then, one by one using a loop similar to the second code example above, your content would inspect each statement, ignore the ones where the result doesn’t contain that answer, and count the ones where it does. In the case of finding out when Anthony put Craig through orientation, you’d have to, for example, query for those statements where Anthony is an included agent. Then, one by one, inspect each statement to see if it also includes Craig as an agent. So you can query for these more complicated statements, but it takes some extra effort.

In conclusion

Queries are where xAPI really shines. Yes, sending the data is important. And xAPI makes collecting data on user interactions much easier than SCORM, or several other non-standard or proprietary options. But what good is data if you can’t use it? Queries do have their limits. And those limits are vital to know and understand. By knowing what those limits are, you can start to build more solid strategies for what data you collect. If you know how you will use the data, you know how you need to collect it. So by understanding how queries work and what their limitations are, you can begin to build much better, and more meaningful, statements. In the next (and final) entry of this series, we’ll take a more practical look at these limitations and how they can drive your data collection. We’ll see queries in action. And we’ll see how you can use data from two different activities to build a useful report that can tell you whether or not your content is doing its job!

Bonus

For those of you who would like to see it, here is the complete code for the examples I used in this article.

  
<!doctype html>
<head>
  <title>xAPI sample page</title>
  	<!-- Includes for ADL's xAPI Wrapper -->
	<!-- Download the files from: -->
	<!-- https://github.com/adlnet/xAPIWrapper -->
	<script type="text/javascript" src="./js/cryptojs_v3.1.2.js"></script>
	<script type="text/javascript" src="./js/xapiwrapper.js"></script>
	<!------------------------------------->
	
	<script>
		function config_LRS(){
		  
			  var conf = {
				  "endpoint" : "https://lrs.adlnet.gov/xapi/",
				  "auth" : "Basic " + toBase64('xapi-tools:xapi-tools'),
			  };
			  ADL.XAPIWrapper.changeConfig(conf);
		}
							
		function get_statements(){
			config_LRS();
			
			// Set the query parameters
			var myparams = ADL.XAPIWrapper.searchParams()
			var d = new Date("July 19, 2017 12:00:00");
			myparams['since'] = d.toISOString();
			myparams['verb'] = 'http://adlnet.gov/expapi/verbs/answered';
			myparams['activity'] = 'http://adlnet.gov/expapi/activities/example';
			myparams['agent'] = '{"mbox": "mailto:aa_altieri@outlook.com"}';
	//		** NOTE: For agent search, it must send as an object.  You MUST include the 
	//		**  outer single quotes!!!
	//		**********
// End of query paramaters

			//send the query
			var ret = ADL.XAPIWrapper.getStatements(myparams);
			
			//process and display results
			var txt = " ";
			if (ret) {
				for (i = 0; i < ret.statements.length; i++) {
					var stmt = 	i + ".   " +
								ret.statements[i].actor.mbox + "         " +
								ret.statements[i].verb.id + "         " +
								ret.statements[i].object.id + "         " +
								ret.statements[i].timestamp + "<br>";	
					txt += stmt;
					}
				document.getElementById("results").innerHTML = txt;
				
				// console debug statements
				console.log (ret.statements);
				console.log (ret.statements.length);
				console.log (ret.statements[0].actor.mbox);			
			}
		}
	</script>
</head>

<body>
	<button type="button" onclick="get_statements()">Get Statements</button>
	<br/><br/><br/>
	<div id="results"> </div>
</body>
</html>

From the editor: Want more?

Learn more about using xAPI to control your content at DevLearn 2017 Conference & Expo, October 25 – 27 in Las Vegas. Sessions include: