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.


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('', port, log_level=logging.INFO)
hello_workflow = relay.workflow.Workflow('hello workflow')
wf_server.register(hello_workflow, '/hellopath')

interaction_name = 'hello interaction'

async def start_handler(workflow, trigger):
    target = workflow.make_target_uris(trigger)
    await workflow.start_interaction(target, interaction_name)

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()

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)
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());


    private static class MyWorkflow extends Workflow {
        public void onStart(Relay relay, StartEvent startEvent) {
            super.onStart(relay, startEvent);

            String sourceUri = Relay.getSourceUriFromStartEvent(startEvent);
            relay.startInteraction( sourceUri, "hello interaction", null);

        public void onInteractionLifecycle(Relay relay, InteractionLifecycleEvent lifecycleEvent) {
            super.onInteractionLifecycle(relay, lifecycleEvent);

            String interactionUri = (String)lifecycleEvent.sourceUri;
            if (lifecycleEvent.isTypeStarted()) {
                relay.say(interactionUri, "Hello world");
            if (lifecycleEvent.isTypeEnded()) {
// Copyright © 2022 Relay Inc.

package main

import (

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)

            if interactionLifecycleEvent.LifecycleType == "ended" {

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:// (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 your package.json file. If you created your own package.json using npm init, make sure to add this line. The package.json file included in the relay-samples repository already includes this setting.


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 the wss protocol (WebSocket Secure) because the unencrypted ws 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/] OnOpen 
14:58:05.674 [11] [] [INF] [906ad4d4-8f24-4003-86c2-4f28b191f4d3/] 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.