leej

Alexa, start a Nintex Workflow

Blog Post created by leej Support on Oct 13, 2016

I enjoy having the ability to speak to a room and auto-magically set a cooking timer, play NPR, or check the weather. I then thought it would be amusing to be able to start a workflow as well. If I can order a pizza with my voice, I should be able to start a workflow too!

 

With the Nintex Workflow Cloud, the ability to externally start a workflow (Nintex Workflow Cloud External Start ) opens up a world cloud of possibilities. With the right tools, anything can become a start event. In this blog post, I'll walk you through how I wired up Alexa to successfully start a Nintex Workflow with code samples and Alexa skills kit examples.

 

 

Alexa Skill Development

For demo purposes, I'll be calling into a leave approval workflow built in NWC. To build the Alexa skill that will help kick-off this workflow, we need a few important bits of information:

Skill Info

  • Skill Type - What type of skill you're making.
    • Select: Custom Interaction Model
  • Name - Something of your choosing.
    • In my case, "Time off"
  • Invocation Name: Something of your choosing.
    • In my case, "for time off"

Interaction Model 

This defines the voice interface for the Alexa skill and it is built with the following sub-parts:

  • Intent Schema - This defines the user intents, which define slots (variables) tied to each intent (function).
  • Sample Utterances - This defines the phrases the user can speak and tells Alexa where to expect the given slots for your various intents.

Intent Schema Example

{  
   "intents":[ 
      { 
         "intent":"DayOff",
         "slots":[ 
            { 
               "name":"Day",
               "type":"AMAZON.DATE"
            }
         ]
      },
      { 
         "intent":"DaysOff",
         "slots":[ 
            { 
               "name":"DayStart",
               "type":"AMAZON.DATE"
            },
            { 
               "name":"DayEnd",
               "type":"AMAZON.DATE"
            }
         ]
      }
   ]
}

 

Sample Utterances Example

DayOff ask for time off {Day}
DayOff ask for {Day} off
DayOff I want {Day} off
DayOff I want a vacation {Day}
DayOff I want to take a vacation {Day}
DaysOff ask for time off from {DayStart} to {DayEnd}
DaysOff ask for time off starting {DayStart} until {DayEnd}
DaysOff ask for vacation from {DayStart} to {DayEnd}
DaysOff ask for vacation starting {DayStart} until {DayEnd}
DaysOff I want time off from {DayStart} to {DayEnd}
DaysOff I want time off starting {DayStart} until {DayEnd}
DaysOff I want to take vacation from {DayStart} to {DayEnd}
DaysOff I want to take vacation starting {DayStart} until {DayEnd}
DaysOff I want a vacation from {DayStart} to {DayEnd}
DaysOff I want a vacation starting {DayStart} until {DayEnd}

Configuration

  • Service Endpoint Type: HTTPS (I haven't learned AWS Lambda yet  )
    • HTTPS URL: North America
      • This is used to give Alexa interactions a better response time by directing users' requests to a particular geographical region. European designers may want to point to Europe here.
      • In my sample, I'm pointing to an endpoint I created at ngrok since my demo service runs locally.
      • Note: For test purposes, you may want to point to an HTTPS endpoint inspector such as Hookbin - Capture and Inspect HTTP Requests .
  • Account Linking: No

 

SSL Certificate

Sample Request from Amazon

Once the above configuration is complete, you can go ahead and test what Amazon will send you. Here's a sample of the JSON body:

{
    "version": "1.0",
    "session": {
        "new": true,
        "sessionId": "SessionId.9adaf721-b8bb-482b-b35c-8d54c8bed912",
        "application": {
            "applicationId": "amzn1.ask.skill.<intentionally_hidden_uuid>"
        },
        "attributes": {},
        "user": {
            "userId": "amzn1.ask.account.<intentionally_hidden_token>"
        }
    },
    "request": {
        "type": "IntentRequest",
        "requestId": "EdwRequestId.f4b67ca1-3420-4d68-89ff-6cc660784d74",
        "timestamp": "2016-10-13T01:24:07Z",
        "locale": "en-US",
        "intent": {
            "name": "DayOff",
            "slots": {
                "Day": {
                    "name": "Day",
                    "value": "2016-10-14"
                }
            }
        }
    }
}

 

Nintex Workflow Development

 

Workflow Example

Now that's the Alexa/Amazon-side of things complete. Let's have a look at our sample workflow below. Quick explanation of the Run-If action. I simply ignore the DayEnd variable if it is less than DayStart (it doesn't make sense to take end a vacation before it starts). When a date isn't defined, it is set to "0001-01-01 00:00:00". That conveniently will fail the Run-If check, allowing me to have both single day off and multiple days off scenarios covered by the same workflow that takes two (start & end) variables. 

nwc-leave-workflow-example

 

Start event Configuration

More importantly, let's have a look at the start variables that we've defined in the External Start event below. The DayEnd and DayStart variables are accepted as DateTime (ISO 8601, I believe) variables.

 

These appear in the external start's swagger API under the following path:

paths/<wf_id/instances>/post/parameters

 

Swagger API Example

{
  "swagger": "2.0",
  "info": {
    "title": "LeaveApprovalTest-Alexa",
    "description": "",
    "version": "1.0.0"
  },
  "schemes": [
    "https"
  ],
  "basePath": "/api/v1/workflow/published",
  "produces": [
    "application/json"
  ],
  "consumes": [
    "application/json"
  ],
  "host": "<my-domain>.workflowcloud.com",
  "paths": {
    "/2da2e395-9e86-4f2d-bb2e-18e499acac82/instances": {
      "post": {
        "summary": "Starts the workflow",
        "description": "Starts workflow: LeaveApprovalTest-Alexa",
        "operationId": "wf2da2e395-9e86-4f2d-bb2e-18e499acac82",
        "parameters": [
          {
            "name": "API Parameters",
            "required": true,
            "in": "body",
            "schema": {
              "type": "object",
              "properties": {
                "startData": {
                  "type": "object",
                  "properties": {
                    "se_day_end1": {
                      "title": "DayEnd",
                      "description": "",
                      "type": "string",
                      "format": "date-time"
                    },
                    "se_day_start1": {
                      "title": "DayStart",
                      "description": "",
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "options": {
                  "type": "object",
                  "properties": {
                    "callbackUrl": {
                      "title": "callbackUrl",
                      "description": "A Url to return the results back (https urls only)",
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          {
            "name": "token",
            "type": "string",
            "in": "query",
            "description": "A security token to start the workflow"
          }
        ],
        "responses": {
          "202": {
            "description": "Accepted",
            "x-ntx-callback-schema": {
              "type": "object",
              "properties": {
                "returnData": {
                  "type": "object",
                  "properties": {
                    "se_day_end1": {
                      "title": "DayEnd",
                      "description": "",
                      "type": "string",
                      "format": "date-time"
                    },
                    "se_day_start1": {
                      "title": "DayStart",
                      "description": "",
                      "type": "string",
                      "format": "date-time"
                    },
                    "c_duration_builder1": {
                      "title": "DurationBuilder",
                      "description": "",
                      "type": "string"
                    },
                    "c_placeholder_var1": {
                      "title": "PlaceholderVar",
                      "description": "",
                      "type": "string"
                    }
                  }
                },
                "workflow": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad Request"
          },
          "404": {
            "description": "Not Found"
          },
          "410": {
            "description": "Gone"
          },
          "429": {
            "description": "Too Many Requests"
          },
          "503": {
            "description": "Service Unavailable - Overloaded"
          },
          "default": {
            "description": "Unexpected Error"
          }
        }
      }
    }
  }
}

 

Looking at lines 35 through 47 above, we see the names of our two variables we need:

se_day_start1, se_day_end1

 

Bridging Two Worlds Clouds

Here's the challenge:

NWC expects the data in swagger format, but Amazon gives us data in their own JSON-structured format. Because of this mismatch, there needs to be an intermediate service that will help map Amazon's data into a format that can be consumed by NWC.

 

My solution was to write a simple python script to do the translation from one format to another. The code is pasted below:

 

Note: I've used Python 3.5 and Flask for this. It shouldn't be too challenging to adapt this to the language of your choice (as long as the language of choice has a good way of dealing with requests/responses and json).

 

Python 3.5 + Flask Example

from flask import Flask
from flask import request
from flask import Response
from flask import jsonify
import json
import requests
import datetime

app = Flask(__name__)

# This is a test route
@app.route('/')
def hello_world():
    return 'Hello World!'

# This handles the endpoint that the Alexa skill will contact
@app.route('/dayoff', methods=['POST'])
def dayoff():

    # The important part of the Alexa call is the JSON body
    content = request.get_json(silent=False)

    # I print this to console for sanity-checking, but this can be safely removed
    print(content)

    # Grab the intent name that's being called (one of two) from the interaction model
    intentName = content['request']['intent']['name']

    # Branch based on what intent has been called
    if intentName == 'DaysOff':
        # This handles the date-range style of asking for time off (start date, end date)
        dayA = content['request']['intent']['slots']['DayStart']['value']
        dayB = content['request']['intent']['slots']['DayEnd']['value']

    elif intentName == 'DayOff':
        # This handles the single-date style of asking for time off (same start and end date)
        dayA = content['request']['intent']['slots']['Day']['value']
        # This is a little hack I'm doing to have the workflow use logic to take two date variables
        # If dayB (DayEnd) is before dayA (DayStart), the leave doesn't make sense, so treat it as a single day off
        dayB = datetime.datetime.min

    # This calls the actual function to send an external start payload
    startLeaveWorkflow(dayA, dayB)

    # This is a manually constructed response for Alexa to report back to the user
    data = {'version': '1.0',
            'response':
                {
                    'outputSpeech':
                        {
                            'type': 'PlainText',
                            'text': 'Request received, time off request made'
                        }
                }
            }
    # This is a helper function to turn the json-style `data` above into valid json
    js = json.dumps(data)

    # Return a successful response to Alexa to read back
    return Response(js, content_type='application/json;charset=UTF-8', mimetype='application/json', status=200)


def startLeaveWorkflow(dayA, dayB):

    # Craft the json payload for the NWC external start
    post_data = '{"startData": {"se_day_end1": "' + str(dayB) + '", "se_day_start1": "' + str(dayA) + '"},"options": {}}'

    # Sanity-check by printing to console
    print(post_data)

    # Set the proper request header
    headers = {'content-type':'application/json'}

    # Make the external start request
    res = requests.post('https://<my-nwc-domain>.workflowcloud.com/api/v1/workflow/published/<my-workflow-id>/instances?token=<my-auth-token>', data=post_data, headers=headers)

    # Print the response status code and raw response for sanity-checking
    print(res.status_code)
    print(str(res.raw))
    return


if __name__ == '__main__':
    app.run()

 

Note that for line 75, I've modified the URL to remove sensitive bits.

res = requests.post('https://<my-nwc-domain>.workflowcloud.com/api/v1/workflow/published/<my-workflow-id>/instances?token=<my-auth-token>', data=post_data, headers=headers)

This URL can be crafted from the External Start URL that is generated for your workflow. All you need to do is change

/swagger.json

to

/instances

 

So this:

https://<my-domain.workflowcloud.com/api/v1/workflow/published/<my-workflow-id>/swagger.json?token=<my-auth-token>

Becomes:

https://<my-domain.workflowcloud.com/api/v1/workflow/published/<my-workflow-id>/instances?token=<my-auth-token>

 

Connecting the (Echo) Dot(s)

Once all the dots (well, an Echo Dot, a Python app, and a NWC published workflow) were in place, all that was necessary was to make sure that my Python app could receive traffic from Amazon. Back to ngrok for this one. I just needed to have an ngrok tunnel running. I downloaded ngrok for my OS and ran the following command:

./ngrok http 5000

Simply put, that takes all HTTP(S) traffic and forwards it on to port 5000 on your local machine. You may need to change your port, depending on how your expose your Python (or equivalent) app. If all the dots are successfully connected, you should see something like the output below in your terminal, along with as many 200 OK responses as correct voice requests you've made!

ngrok by @inconshreveable

Session Status                online                                     
Version                       2.1.14                                     
Region                        United States (us)                         
Web Interface                 http://127.0.0.1:4040                      
Forwarding                    http://19a407fb.ngrok.io -> localhost:5000 
Forwarding                    https://19a407fb.ngrok.io -> localhost:5000
           
Connections                   ttl     opn     rt1     rt5     p50     p90
                              80      0       0.00    0.00    0.01    2.28
           
HTTP Requests                                                            
-------------                                                            
           
POST /dayoff                   200 OK                                    
POST /dayoff                   200 OK        

 

Now, if you'll excuse me, I need to go bombard my manager with a bunch of spurious leave requests.  

Outcomes