This blog explains how you can create, install, and maintain APL widgets for your Alexa skills.
Prerequisites:
If you have built APL documents before, you should be familiar with the concept of datasources. These allow developers to bind external data to an APL document without the need to change its structure.
This feature is usually referred to as data-binding, which is explained in more detail in this blog post and this video.
While widgets still support datasources, they can also read from the datastore, which represents a local storage area accessible by your widget. You are responsible for keeping the datastore up to date if your widget needs to show different information over time, and you will learn how to do this as well.
For reference, this is the JSON representation of a widget supporting the datastore:
{
"document": {
"type": "APL",
"version": "2022.2",
"extensions": [
{
"name": "DataStore",
"uri": "alexaext:datastore:10"
}
],
"settings": {
"DataStore": {
"dataBindings": [
{
"namespace": "Widget",
"key": "WidgetKey",
"dataBindingName": "WidgetDataStoreData",
"dataType": "object"
}
]
}
},
"import": [
{
"name": "alexa-layouts",
"version": "1.6.0"
}
],
"mainTemplate": {
"parameters": [
"WidgetDefaultData"
],
"items": {
"type": "Container",
"width": "100vw",
"height": "100vh",
"id": "rootContainer",
"direction": "row",
"item": [
{
"type": "Frame",
"backgroundColor": "${WidgetDataStoreData.frameColor ? WidgetDataStoreData.frameColor : WidgetDefaultData.frameColor}",
"position": "absolute",
"width": "100vw",
"height": "100vh"
},
{
"type": "Container",
"width": "100vw",
"height": "100vh",
"items": [
{
"text": "${WidgetDataStoreData.text ? WidgetDataStoreData.text : WidgetDefaultData.text}",
"textAlign": "center",
"textAlignVertical": "center",
"type": "Text",
"width": "100%",
"height": "100%"
}
]
}
]
}
}
},
"datasources": {
"WidgetDefaultData": {
"frameColor": "green",
"text": "Hello from datasources"
}
}
}
As you can see, the widget’s document block has the extensions and settings objects with datastore information, which is something you don’t usually see on regular APL documents.
The datasources part instead holds the default values for the background color and text that the widget will render as soon as it’s installed.
With this configuration the widget’s default text will be “Hello from datasources”, but it will change to whatever you include into the datastore update payload required to keep the widget up to date (more on that later).
The key to making this work relies on the expression currently set for the “text” property of the Text component (reported below):
"text":"${WidgetDataStoreData.text ? WidgetDataStoreData.text : WidgetDataSourcesData.text}"
This means that the widget will read the information from the datastore if the latter is populated. If the datastore is empty, the widget will just render the text present in the datasources (as in, “Hello from datasources”).
The same mechanism applies to the Frame component as well: it will read the backgroundColor from the datastore if populated, or from the datasources if the datastore is empty.
Now that you know how the widget can retrieve data from different areas, let’s move on and associate it to an actual skill.
Since an APL widget needs be associated to a skill, you must create one choosing one the following methods (skip if you have a skill already):
Once your skill is created, make sure to enable the “Alexa Presentation Language” interface from the “Interfaces” section of the “Build” tab, then save and re-build the skill package by pressing the “Build skill” button.
You are now ready to start designing your widget. Open the “Build” tab of the developer console again, click on “Multimodal Responses”, then “Widget” and eventually “Create Widget”.
You will see different templates; just press on “Blank Document” to start from scratch and the APL Authoring Tool for widgets will open.
From there, paste the content of “document” and “datasources” object from the paragraph above respectively into the “APL” and “DATA” sections. Alternatively, save this JSON file and import it using the “Upload” button.
The rendered widget should look like this:
The widget is expectedly showing “Hello from datasources” with a green background, and this information is retrieved from the datasources. Press the save button on the upper-right corner to associate this widget to your skill.
Note that each structural change made to the widget requires you to re-build the skill, so please do that as soon as the widget is saved, otherwise its installation (covered in the next paragraph) will either fail or install an older version of it.
To install the widget, just select the target device on the bottom drop-down menu, click “Install” and then “Send to Device”.
In a few minutes you should see that the widget has reached your widget gallery. Great job, your widget prototype is now installed in your device!
Note that your backend will receive a Alexa.DataStore.PackageManager.UsagesInstalled request for each time a customer installs your widget.
The widget’s datastore can be updated from the Authoring Tool by pressing the “Update Data Store” button, which requires the following payload
[
{
"type": "PUT_OBJECT",
"namespace": "Widget",
"key": "WidgetKey",
"content": {
"frameColor": "blue",
"text": "Hello from datastore"
}
}
]
Note how the values of namespace and key are respectively matching the ones present in the settings section of the widget’s document.
This will change the background to blue and the text to “Hello from datastore”, but will only target the device selected in the drop-down menu of the developer console. This mechanism is meant to be used for the development stage only.
In a production environment you should update the datastore using the Data Store REST API, which requires authentication against the Alexa API Endpoint and allows two different kind of targets:
“DEVICE”:
Targets one or multiple device IDs (up to 20 for each API call)
“USER”:
Targets all of the devices associated to a given userid
1) Retrieve an access token from the Alexa API Endpoint:
const axios = require('axios');
// Get those from the "Alexa Skill Messaging" section in Build --> Tools --> Permissions
const skillClientID = ""
const skillClientSecret = ""
// Change the endpoint based on your region
// NA: https://api.amazonalexa.com
// EU: https://api.eu.amazonalexa.com
// FE: https://api.fe.amazonalexa.com
function getAccessToken() {
let config = {
method: "post",
url: "https://api.amazon.com/auth/o2/token",
timeout: 3000,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
charset: "utf-8",
},
params: {
grant_type: "client_credentials",
client_id: skillClientID,
client_secret: skillClientSecret,
scope: "alexa::datastore"
}
};
return axios(config)
.then(function (response) {
return response.data;
})
.catch(function (error) {
console.log(error)
});
}
const tokenResponse = await getAccessToken();
2) POST the PUT_OBJECT payload to the Alexa API Endpoint:
let dataStoreUpdateCommands = [
{
type: "PUT_OBJECT",
namespace: "Widget",
key: "WidgetKey",
content: {
frameColor: "blue",
text: "Hello from datastore"
},
},
];
let dataStoreUserTarget = {
type: "USER",
id: handlerInput.requestEnvelope.context.System.user.userId,
};
let dataStoreDeviceTarget = {
type: "DEVICE",
id: handlerInput.requestEnvelope.context.System.device.deviceId,
};
async function updateDatastore(token, commands, target) {
const config = {
method: "post",
url: `https://api.amazonalexa.com/v1/datastore/commands`,
headers: {
"Content-Type": "application/json",
Authorization: `${token.token_type} ${token.access_token}`,
},
data: {
commands: commands,
target: target,
},
};
return axios(config)
.then(function (response) {
console.log(JSON.stringify(response.data));
return response.data;
})
.catch(function (error) {
console.log(error);
});
}
await updateDatastore(tokenResponse, dataStoreUpdateCommands, dataStoreUserTarget);
Note that the snippet above is using the dataStoreUserTarget object to target a unique userId. You can use dataStoreDeviceTarget to target a specific deviceId instead.
To remove data from the datastore, follow the same steps from the “Updating the widget’s datastore“ section, but use the REMOVE_OBJECT type instead. Here is an example payload:
[
{
"type": "REMOVE_OBJECT",
"namespace": "Widget",
"key": "WidgetKey"
}
]
Like regular APL, widgets support the TouchWrapper component.
When a user taps on a Widget, the latter can generate two different kinds of interactions:
“STANDARD“:
The skill backend will receive an Alexa.Presentation.APL.UserEvent request, and can respond with regular skill responses, including an output speech.
“INLINE“:
The skill backend will receive a Alexa.Presentation.APL.UserEvent request, which can only be responded with a background action, such as querying a database or updating the datastore.
The type of interaction (“STANDARD“ vs “INLINE“) is declared in a flag inside the SendEvent APL command. See interactionMode object in the example below:
{
"type": "TouchWrapper",
"id": "myTouchWrapper",
"spacing": "@spacingSmall",
"alignSelf": "center",
"onPress": [
{
"type": "SendEvent",
"arguments": [
"Send this data to the skill"
],
"flags": {
"interactionMode": "INLINE" // or STANDARD
},
"components": [
"myText"
]
}
],
"item": {
"type": "Text",
"id": "myText",
"text": "Tap to send a UserEvent to the skill."
}
}
APL Widgets are an awesome way to enhance your skill and create additional entry points to your experience. Now that you have all that you need to create, install, and maintain an APL widget for your skill, I would suggest having a look at the additional resources below to boost your APL knowledge and create even richer visuals both for your widgets and your skills.
Add Visuals and Audio to Your Skill
About Widgets and Alexa Presentation Language (APL)
APL Widgets Reference