Writing a custom workflow
Now that you have run a built-in workflow after registering it, you can proceed to write your own custom workflow.
Setup
In case you are skipping around in this documentation, proceeding here does require you to have the CLI installed and logged in.
Pick the language you want to use in the tabs below. You can find the list of SDK languages here.
First, we'll create our project in a directory of our choosing:
$ mkdir myworkflow
$ cd myworkflow
$ npm init
$ npm install @relaypro/sdk
$ npm install express (for the websocket implementation)
$ cd samples
$ vi helloworld_standalone.js (pick your favorite text editor)
$ mkdir myworkflow
$ cd myworkflow
$ git clone [email protected]:relaypro/relay-py.git (isn't in PyPI yet)
$ cd relay-py
$ cd samples
$ vi hello_world_wf.py (pick your favorite text editor)
$ mkdir myworkflow
$ cd myworkflow
$ git clone [email protected]:relaypro/relay-dotnet.git (not in Nuget yet)
$ cd relay-dotnet
$ code ConsoleAppSamples/Program.cs (pick your favorite text editor)
$ mkdir myworkflow
$ cd myworkflow
$ git clone [email protected]:relaypro/relay-java.git (not in Maven yet)
$ cd relay-java
$ cd app/src/main/java/com/relaypro/app/examples/standalone
$ vi HelloWorld.java (pick your favorite text editor)
$ cd myworkflow
$ git clone [email protected]:relaypro/relay-go.git
$ cd relay-go
$ cd samples
$ vi helloworld.java (pick your favorite text editor)
Hello World Code
Now you can start writing your first workflow. Here we're creating a simple Hello World workflow. In this sample we're simply asking the device to say "hello world" out loud via text-to-speech.
import { relay, Event, createWorkflow, Uri } from '@relaypro/sdk'
const app = relay()
const helloWorkflow = createWorkflow(workflow => {
const interactionName = 'hello interaction'
workflow.on(Event.START, async (event) => {
const { trigger: { args: { source_uri } } } = event
await workflow.startInteraction([source_uri], interactionName)
})
workflow.on(Event.INTERACTION_STARTED, async ({ source_uri }) => {
await workflow.sayAndWait(source_uri, 'hello world')
await workflow.endInteraction([source_uri])
})
workflow.on(Event.INTERACTION_ENDED, async() => {
await workflow.terminate()
})
})
app.workflow(`hellopath`, helloWorkflow)
import relay.workflow
import os
import logging
port = os.getenv('PORT', 8080)
wf_server = relay.workflow.Server('0.0.0.0', port, log_level=logging.INFO)
hello_workflow = relay.workflow.Workflow('hello workflow')
wf_server.register(hello_workflow, '/hellopath')
interaction_name = 'hello interaction'
@hello_workflow.on_start
async def start_handler(workflow, trigger):
target = workflow.make_target_uris(trigger)
await workflow.start_interaction(target, interaction_name)
@hello_workflow.on_interaction_lifecycle
async def lifecycle_handler(workflow, itype, interaction_uri, reason):
if itype == relay.workflow.TYPE_STARTED:
await workflow.say_and_wait(interaction_uri, 'hello world')
await workflow.end_interaction(interaction_uri)
if itype == relay.workflow.TYPE_ENDED:
await workflow.terminate()
wf_server.start()
using RelayDotNet;
namespace SamplesLibrary
{
public class HelloWorldWorkflow : AbstractRelayWorkflow
{
public HelloWorldWorkflow(Relay relay) : base(relay)
{
}
public override void OnStart(IDictionary<string, object> dictionary)
{
var trigger = (Dictionary<string, object>) dictionary["trigger"];
var triggerArgs = (Dictionary<string, object>) trigger["args"];
var deviceUri = (string) triggerArgs["source_uri"];
Relay.StartInteraction(this, deviceUri, "hello world", new Dictionary<string, object>());
}
public override async void OnInteractionLifecycle(IDictionary<string, object> dictionary)
{
var type = (string) dictionary["type"];
if (type == InteractionLifecycleType.Started)
{
var interactionUri = (string) dictionary["source_uri"];
await Relay.SayAndWait(this, interactionUri, "Hello world");
Relay.EndInteraction(this, interactionUri);
}
else if (type == InteractionLifecycleType.Ended)
{
Relay.Terminate(this);
}
}
}
}
package com.relaypro.app.examples.standalone;
import com.relaypro.app.examples.util.JettyWebsocketServer;
...
import java.util.Map;
public class HelloWorld {
public static void main(String... args) {
int port = 8080;
Map<String, String> env = System.getenv();
try {
if (env.containsKey("PORT") && (Integer.parseInt(env.get("PORT")) > 0)) {
port = Integer.parseInt(env.get("PORT"));
}
} catch (NumberFormatException e) {
System.err.println("Unable to parse PORT env value as an integer, ignoring: " + env.get("PORT"));
}
Relay.addWorkflow("hellopath", new MyWorkflow());
JettyWebsocketServer.startServer(port);
}
private static class MyWorkflow extends Workflow {
@Override
public void onStart(Relay relay, StartEvent startEvent) {
super.onStart(relay, startEvent);
String sourceUri = Relay.getSourceUriFromStartEvent(startEvent);
relay.startInteraction( sourceUri, "hello interaction", null);
}
@Override
public void onInteractionLifecycle(Relay relay, InteractionLifecycleEvent lifecycleEvent) {
super.onInteractionLifecycle(relay, lifecycleEvent);
String interactionUri = (String)lifecycleEvent.sourceUri;
if (lifecycleEvent.isTypeStarted()) {
relay.say(interactionUri, "Hello world");
relay.endInteraction(interactionUri);
}
if (lifecycleEvent.isTypeEnded()) {
relay.terminate();
}
}
}
}
// Copyright © 2022 Relay Inc.
package main
import (
"relay-go/pkg/sdk"
)
var port = ":8080"
func main() {
sdk.AddWorkflow("hellopath", func(api sdk.RelayApi) {
api.OnStart(func(startEvent sdk.StartEvent) {
sourceUri := api.GetSourceUri(startEvent)
api.StartInteraction(sourceUri, "hello interaction")
})
api.OnInteractionLifecycle(func(interactionLifecycleEvent sdk.InteractionLifecycleEvent) {
if interactionLifecycleEvent.LifecycleType == "started" {
interactionUri := interactionLifecycleEvent.SourceUri
api.SayAndWait(interactionUri, "Hello World", sdk.ENGLISH)
api.EndInteraction(sourceUri)
}
if interactionLifecycleEvent.LifecycleType == "ended" {
api.Terminate()
}
})
})
sdk.InitializeRelaySdk(port)
}
Save the changes to the source file you made with your editor.
Now compile and run your workflow on your local workstation. You should see something like this:
$ node ./helloworld_standalone.js
Relay SDK WebSocket Server listening => 8080
$ python3 ./hello_world_wf.py
INFO:relay.workflow:Relay workflow server (relay-sdk-python/2.0.0) listening on localhost port 8080 with plaintext
$ dotnet build ConsoleAppSamples
$ cd ConsoleAppSamples (so the file appsettings.json can be found)
$ dotnet ./bin/Debug/net5.0/ConsoleAppSamples.dll
11:11:42 [1] [] [INF] Add "/hellopath" to SamplesLibrary.HelloWorldWorkflow
7/13/2022 11:11:42 AM [Info] Server started at ws://0.0.0.0:8080 (actual port 8080)
$ make build
$ make run
2022-10-28 14:36:49 INFO JettyWebsocketServer:44 - Starting server on port 8080
$ go run helloworld.go
INFO[0000] Added workflow named hellopath map is map[coffeepath:0x6d29c0]
INFO[0000] starting http server on :8080
Node.js Tip
The Node.js Javascript sample code above uses ES6 import syntax, which requires you to specify
"type": "module"
in yourpackage.json
file. If you created your ownpackage.json
usingnpm init
, make sure to add this line. Thepackage.json
file included in the relay-samples repository already includes this setting.
Ngrok
Given that you are likely sitting behind a network firewall in your office or at home, your workflow server won't be reachable from the Relay server. Additionally, since the Relay server requires TLS encryption for all its communication, your workflow server will need a TLS certificate. To easily deal with these two requirements, we'll expose our workflow server to the Relay server using a utility called ngrok. You don't have to use ngrok, but it does provide an easy way to get your workflow application reachable on the internet with a built-in TLS certificate. See the section on ngrok for more details on how to set it up:
$ ngrok http 8080
You should see a randomized public URL for your workflow, forwarded to your local machine. We'll use this in the next step to register our workflow. Here, we're using the phrase trigger 'hello' on the device and pointing that to our workflow. To get usage details, you can use the --help
on any Relay CLI command.
It doesn't matter which SDK language you are using, this will work for all of them. The only thing that does matter is that the port number used in the ngrok
command above matches what your workflow is listening on.
Workflow Server Requirements
A few notes about what is required on your workflow server.
- Your workflow server must be reachable on the Internet from the Relay server. If your server is behind a firewall, then you need a way to expose it. A tool like ngrok may help you do that during development. Or you can get your workflow server hosted externally with a service like Heroku, AWS EC2, Cloudflare, etc.
- Your workflow server needs to have TLS (a server certificate) for the Relay server to connect to it. This is because the Relay server is enforcing that all traffic between it and your workflow server be encrypted. When registering your workflow on the CLI with the
relay workflow create
command, the URL parameter must use thewss
protocol (WebSocket Secure) because the unencryptedws
protocol isn't allowed. If you don't already have a server certificate, Let's Encrypt may help you do that.- For all these items, please consult with your I/T staff for processes and best practices required by your organization.
Registering Your Workflow
Now, you can try registering your custom Hello World workflow with the Relay server, using the following commands (replacing the URL with the address from ngrok, but keeping the hellopath
part):
$ relay help workflow create phrase
$ relay workflow create phrase --trigger "demo" --name hello-phrase --uri 'wss://myserver.myhost.com/helloworld' --install-all
=== Installed Workflow
ID Name Type Uri Args Installed on
wf_hello_ONkUx9CWHQLYtdPINECmqkC hello-phrase phrase:demo wss://myserver.myhost.com/helloworld all devices
Try It Out
Nice work! Now you can go to your device and try it out. To use a phrase trigger, either navigate to the "Relay Assistant" channel and use the big talk button on the front of the device, or from any channel press and hold the Assistant button (the button between the '+' and '-' keys on the side) and speak your trigger phrase of 'demo'.
And you should see a couple lines added to the logs each time the workflow is run:
Workflow new connection on /hellopath
Workflow closed connection on /hellopath
INFO:relay.workflow:[hello workflow:140081351776576] workflow instance started for /hellopath
INFO:relay.workflow:[hello workflow:140081351776576] workflow instance terminated
14:57:54.800 [7] [] [INF] [906ad4d4-8f24-4003-86c2-4f28b191f4d3/127.0.0.1:49664/hellopath] OnOpen
14:58:05.674 [11] [] [INF] [906ad4d4-8f24-4003-86c2-4f28b191f4d3/127.0.0.1:49664/hellopath] OnClose
2022-10-28 14:40:29 INFO Relay:106 - Workflow instance started for hellopath
2022-10-28 14:40:30 INFO Relay:110 - Workflow instance terminating, reason: normal
INFO[0000] Added workflow named hellopath map is map[coffeepath:0x6d29c0]
INFO[0000] starting http server on :8080
There are tips available regarding debugging Your workflows.
Updated 11 months ago