Implementing an Alexa skill requires a free developer account with Amazon as well as a secure web service. It is easier by far to let Amazon handle this with Amazon Web Services Lambda. Opening an AWS account requires entering a credit card number, but Amazon says explicitly this service is free for most developers.

Documentation for Alexa Skills Kit (ASK):

https://developer.amazon.com/alexa-skills-kit

Skills can be run by waking Alexa and uttering one of the supported launch words followed by the invocation name. Currently supported launch words include

Ask
Begin
Launch
Load
Open
Play
Resume
Run
Start
Tell
Use

Basic Steps for Implementing a Skill

  1. Create a Lambda function in Amazon Web Services
  2. Enter debugged code in the online form or upload a .ZIP file of code with required Node.js modules
  3. Add Alexa Skills Kit as an event source for the function in the event sources tab and enable the source
  4. Copy the Amazon Resource Name (ARN) of the function
  5. Add a new skill in the Alexa section of apps in the Amazon Developer console
  6. Paste in the ARN of the AWS Lambda function when creating the new skill to link it with the AWS Lambda function
  7. Include a valid intent schema and sample utterances to enable the skill for testing
  8. The skill will appear in the Skills section of the Echo app if your developer account is linked to your main Amazon account

Hello World — Minimal Code

The samples provided by Amazon include a “Hello World” skill, but it is overly dense for what is meant to be the simplest program in any language. Very little code is actually needed to implement a traditional program:

exports.handler = function( event, context ) {
    
    var response = {
        outputSpeech: {
            type: "PlainText",
            text: "Hello World"
        },
        card: {
            type: "Simple",
            title: "Hello World",
            content: "Alexa Skills Kit"
        },
        shouldEndSession: true
    };
    
    context.succeed( { response: response } );
    
};

This could technically be written on one line by putting the JSON response directly into the Node.js succeed method but would lose legibility.

Every skill requires an intent schema with at least one defined intent. The following minimal schema is sufficient for a functional skill:

{ "intents": [ { "intent": "Help", "slots": [] } ] }

Apparently intents need not include the word “intent” in their names. This minimal schema can be accompanied by the minimal sample utterance

Help help

and Alexa will respond appropriately when the skill is launched.


Who’s Your Daddy Now?

Although Amazon says explicitly to avoid invocation names that overlap with built-in services, it can be fun to do so. The “Hello World” code can be used to return any desired response, as for example in response to the above invocation name:

exports.handler = function( event, context ) {
    
    var response = {
        outputSpeech: {
            type: "PlainText",
            text: "(gratuitous response from Alexa)"
        },
        card: {
            type: "Simple",
            title: "Who's Your Daddy Now?",
            content: "(gratuitous response from Alexa)"
        },
        shouldEndSession: true
    };
    
    context.succeed( { response: response } );
    
};

The same minimal intent schema and sample utterance can be used for a functional skill. Be aware that deliberately overlapping with built-in services can lead to erratic behavior depending on how well the invocation name is articulated.


Stock Quotes

A skill of this sort needs an online data source. The public Yahoo Query Language endpoint previously used for this skill was shut down in mid 2017, but an alternate Yahoo resource appears viable, easier to use and has more current information. First the appropriate URL is constructed by appending the array of stock symbols, then the data in HTTP GET response is assembled and parsed as JSON that can be used to construct a spoken response. As of early 2019 a secure request is required to avoid redirection errors.

The third-party Node.js request module could be used here for convenience in handling error messages, but that would require uploading a .ZIP file as opposed to this simple solution in a single file. The code for the output is the same as the last two examples, and has been separated as its own function to indicate this.

exports.handler = function( event, context ) {
    
    var stocks = [ 'AAPL', 'GOOG', 'AMZN' ];

    var https = require( 'https' );
 
    var url = 'https://query1.finance.yahoo.com/v7/finance/quote?symbols=' + stocks;

    https.get( url, function( response ) {
        
        var data = '';
        
        response.on( 'data', function( x ) { data += x; } );

        response.on( 'end', function() {

            var json = JSON.parse( data );

            var text = 'Here are your stock quotes: ';

            for ( var i = 0 ; i < quoteResponse.result.length ; i++ ) {
                var quote = json.quoteResponse.result[i];
                text += quote.longName + ' at ' + quote.regularMarketPrice + ' dollars, a change of '
                        + Math.round( 100 * quote.regularMarketChange ) / 100 + ' dollars. ';
            }
        
            output( text, context );
        
        } );
        
    } );
    
};
    
function output( text, context ) {

    var response = {
        outputSpeech: {
            type: "PlainText",
            text: text
        },
        card: {
            type: "Simple",
            title: "Stocks",
            content: text
        },
        shouldEndSession: true
    };
    
    context.succeed( { response: response } );
    
}

The same minimal intent schema and sample utterance as for the previous two examples can again be used for a functional skill. The format of Alexa’s spoken response can be altered to taste. Having periods at the end of each quote provides a slight pause between quotes.

Financial information from this resource does not appear to be delayed.


Dow Jones Average

Unlike the Yahoo Query Language endpoint previously used for the last skill, the replacement source includes the Dow Jones Industrial Average in what appears to be real time. The code for this skill is mostly the same as for the last one. As of early 2019 a secure request is required to avoid redirection errors.

exports.handler = function( event, context ) {
    
    var https = require( 'https' );
 
    var url = 'https://query1.finance.yahoo.com/v7/finance/quote?symbols=^DJI';

    https.get( url, function( response ) {
        
        var data = '';
        
        response.on( 'data', function( x ) { data += x; } );

        response.on( 'end', function() {

            var json = JSON.parse( data );

            var dow = json.quoteResponse.result[0];
            var upDown = dow.regularMarketChange > 0 ? 'up' : 'down';

            var text = 'The Dow Jones Industrial Average is ' + dow.regularMarketPrice + '. It is ' + upDown + ' '
                        + Math.abs( Math.round( 100 * dow.regularMarketChange ) / 100 ) + ' points, a change of '
                        + Math.abs( Math.round( 100 * dow.regularMarketChangePercent ) / 100 ) + ' percent.';
        
            output( text, context );
        
        } );
        
    } );
    
};
    
function output( text, context ) {

    var response = {
        outputSpeech: {
            type: "PlainText",
            text: text
        },
        card: {
            type: "Simple",
            title: "Dow Jones",
            content: text
        },
        shouldEndSession: true
    };
    
    context.succeed( { response: response } );
    
}

The same minimal intent schema and sample utterance can again be used for a functional skill.


Ask Wolfram

A PHP version of a skill to request information from WolframAlpha has inspired this version entirely in JavaScript. It uses the WolframAlpha API that is free for 2000 requests per month. It requires a free AppID that can be generated in the My Apps section of the Wolfram developer portal.

The skill can be invoked with and without a question. If no question is initially supplied, one will be prompted. The skill remains active for additional questions.

Node.js code for the AWS Lambda function:

exports.handler = function( event, context ) {

    if ( event.request.intent ) {

        question = event.request.intent.slots.Question.value;
        getXML( question, context );

    } else output( 'What is your question?', context );

};

function getXML( input, context ) {

    var http = require( 'http' );
    
    var url = 'http://api.wolframalpha.com/v2/query?input=' + input;
    url += '&format=plaintext&podindex=1,2,3';
    url += '&appid=XXXXXX-XXXXXXXXXX';

    var request = http.get( url, function( response ) {
        
        var xml = '';
        
        response.on( 'data', function( x ) { xml += x; } );
        
        response.on( 'end', function() {
            
            xml = xml.replace( /\n/g, ' ' );

            var pattern = /plaintext>(.*?)<\/plaintext/g;
            var answers = [];
            
            while ( ( match = pattern.exec( xml )) !== null ) {
            
                answers.push( match[1] );
                
            }

            if ( answers[1] === '' ) output( answers[2], context );
            else output( answers[1], context );
        
        } );
        
    } );
  
    request.setTimeout( 4000, function() {
        
        output( 'Your request has timed out. Please try again.', context );
        
    } );
 
}
    
function output( text, context ) {

    var response = {
        outputSpeech: {
            type: "PlainText",
            text: text
        },
        card: {
            type: "Simple",
            title: "Wolfram",
            content: text
        },
        shouldEndSession: false
    };
    
    context.succeed( { response: response } );
    
}

The URL request to the API restricts the returned XML to the top three pods to reduce response time, but you will still probably need to increase the timeout of the AWS Lambda function for a functional skill. The first pod always contains the original question and could be omitted. The second pod generally contains the best answer but will have no plain text response if the top hit is an image. In that case the third pod is used as the response from Alexa.

A minimal intent schema is

{
  "intents": [ {
    "intent": "WhatIs",
    "slots": [ {
       "name": "Question",
       "type": "LITERAL"
    } ]
  } ]
}

Some sample utterances are

WhatIs {what is the atomic weight of boron | Question}
WhatIs {when is sunset tonight | Question}
WhatIs {how tall is mount everest | Question}

The skill cannot be saved if there is any white space between the curly brackets and adjacent words.


Online Etymologies

For students of the English language, one of the best online sources for etymologies is etymonline.com, a rather comprehensive etymological dictionary. For those of us who still read books in traditional paper form (shocking!) it can be convenient to have Alexa perform the lookup.

The site’s server-side script returns searches as HTML description lists. The first etymology on the page is easily retrieved with a nonglobal regular expression. A bit of processing of the response is performed before applying the expression to make the result consistent. Additional processing is done after application of the regular expression to make the output more intelligible when read aloud.

exports.handler = function( event, context ) {

    if ( event.request.intent ) {
        
        word = event.request.intent.slots.Word.value;
        getHTML( word, context );
        
    } else output( 'Which word shall I look up?', context );
    
};

function getHTML( input, context ) {

    var http = require( 'https' );
    
    var url = 'https://www.etymonline.com/search?q=' + input;

    http.get( url, function( response ) {
        
        var html = '';
        
        response.on( 'data', function( x ) { html += x; } );
        
        response.on( 'end', function() {
            
            if ( html.indexOf( '<object>' ) > 0 ) {
       
                var pattern = /<object>(.*?)<\/object>/;
                var match = pattern.exec( html );
                var def = match[1].replace( /<\/?.*?>/g, '' );
         
                def = def.replace( /[0-9]c\./g, ' century' );
                def = def.replace( /n\./g, 'noun' );
                def = def.replace( /v\./g, 'verb' );

                def = def.replace( /"/g, '\'\'' );
                def = def.replace( /'/g, '\'' );
                def = def.replace( /\s\s+/g, ' ' );
      
                output( input + ': ' + def, context );
        
            } else output( 'The word ' + input + ' was not found.', context );

        } );
        
    } );
  
}
    
function output( text, context ) {

    var response = {
        outputSpeech: {
            type: "PlainText",
            text: text
        },
        card: {
            type: "Simple",
            title: "Etymonline",
            content: text
        },
        shouldEndSession: false
    };
    
    context.succeed( { response: response } );
    
}

Be aware that four occurrences of < in the code have been escaped with &lt; to appear correctly within the <pre> tags. These should present no problems when copying from a browser window as opposed to the page source.

The remaining elements of the skill require more thought than previous examples. Alexa does not hear “etymonline” consistently, so an invocation name of “etymology” is recommended. The intent schema is similar to that for the WolframAlpha lookup:

{
  "intents": [ {
    "intent": "Lookup",
    "slots": [ {
       "name": "Word",
       "type": "LITERAL"
    } ]
  } ]
}

Sample utterances do not work consistently on first invocation without a certain number of words. The first two examples here are the most consistent:

Lookup to look up the word {cat|Word}
Lookup to look up the word {dog|Word}
Lookup to look up {cat|Word}
Lookup to look up {dog|Word}
Lookup the word {cat|Word}
Lookup the word {dog|Word}
Lookup {cat|Word}
Lookup {dog|Word}

Since the skill remains active for additional words, the last two can be used in the context of a second search. Attempting to end the skill with the word “quit” will result in a long etymology of that word. The word “exit” is most reliable for initiating a SessionEndedRequest.


Uploaded 2015.06.27 — Updated 2019.01.30 analyticphysics.com