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.
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:
This defines the voice interface for the Alexa skill and it is built with the following sub-parts:
{
"intents":[
{
"intent":"DayOff",
"slots":[
{
"name":"Day",
"type":"AMAZON.DATE"
}
]
},
{
"intent":"DaysOff",
"slots":[
{
"name":"DayStart",
"type":"AMAZON.DATE"
},
{
"name":"DayEnd",
"type":"AMAZON.DATE"
}
]
}
]
}
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}
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"
}
}
}
}
}
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.
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": "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
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).
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>
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.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.