SDK Overview


Software Development Kits are available in the following languages:

Pick one and download it - you can either use git to clone it locally, or reference it in its repository (where available).

Web Management Console (Dash) is the tool to configure and manage devices, groups, as well as many other operations. You login to Dash using your Relay account userid and password.

If you haven't done so already, try logging in to Dash and poke around a bit, especially in the Account -> Users section.


The Relay CLI is the tool specifically for workflow developers to register and manage workflows. It requires the Node.js runtime.

In a following page we'll explain how to install and use the CLI. It is an important tool for your workflows.

Relay Workflow API

The Relay Workflow API lets you create applications that are invoked from Relay devices, mobile apps, and other external sources. The Relay application you write is called a workflow. In this workflow, the Relay APIs are accessed via the Relay SDKs. Before you can run a workflow, you need to use the Relay CLI to register your workflow with the Relay server, and define a trigger for your workflow. When the trigger occurs, the Relay server will instantiate your workflow. At that point of workflow instantiation, a network connection is established from the Relay server to your workflow application server, and the user's Relay device automatically switches to a dynamically-created workflow channel where your workflow instance will run. Events and actions are passed back and forth between the Relay device and your workflow within that channel via the Relay server. At the end of your workflow (either by you sending the terminate command from your workflow application or the user exiting the workflow on the Relay device) the user's Relay exits the dynamic workflow channel and your workflow instance ends.


About this example

Read along with this example, but don't type in anything yet. In the pages that follow later we'll walk you through how to run a built-in workflow, and create your own workflow from scratch, step-by-step, with typing.

Basics of a Workflow

After your workflow is instantiated from a trigger (button press, spoken phrase, etc.), your workflow receives a START event. Your workflow can receive further events, and send actions to a Relay device via API calls. For example:

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

wf_server = relay.workflow.Server('', 8080)
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 sourceUri = (string) dictionary["source_uri"];
                await Relay.SayAndWait(this, sourceUri, "Hello World");
                Relay.EndInteraction(this, sourceUri);
            else if (type == InteractionLifecycleType.Ended)

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" {

As you can probably tell from this example, a workflow is written as a set of callbacks. These callbacks get invoked by the Relay server sending events. All communication with the devices is via the Relay server, so you don't need to worry about establishing and managing connections to the devices yourself, the Relay server will handle that.

This workflow will run on your own server, so you have full control of the code and any connections to your enterprise systems or any of your third-parties that you want to integrate with. That way, those proprietary connections and credentials are never shared with the Relay server or devices, it's all private to you.

When you have your workflow written, hosted, and ready to receive callbacks, you need to register your workflow with the Relay server. The Relay server will constantly monitor the devices for triggers. When the Relay server sees a trigger that you registered for, it will invoke your workflow and pass in information from the originating device. The workflow registration will tell the Relay server which triggers you care about, and the URL of your workflow, so it knows when and where to call your workflow.

Registering a workflow with the Relay server is done using the Relay CLI. You'll use the workflow create command there. Your workflow can be triggered from all devices on your account, or only a specified subset if you want that added measure of control. For the examples here we'll assume you want all devices to be able to trigger to your workflow.

relay workflow create button --trigger=single --name="hello_world" --uri=wss:// --install-all

In the example above, we are registering a new workflow (create). The trigger to look for is of type button, and the trigger for this is 'single', meaning single tap of the talk button. We are giving it a human-readable name hello_world, so you can identify it in a list. Pick any name you want, as long as it is unique. We are specifying the URL where you are hosting the workflow application, so that the Relay server knows where to make a websocket connection to reach your workflow application. This workflow should be available to all the devices on your account (--install-all)

To trigger your workflow now that it is registered, navigate to the Assistant channel and then single-tap the 'talk' button in the center of the device. When the trigger is received and recognized by the Relay server, you should hear a short confirmation beep. The user is then placed in a dynamically-created channel where your workflow will run. On calling the terminate() API or if the user manually exits this channel, the workflow ends.

Looking back at the workflow source code, in the Event.START callback the first thing we do is call the API to start an interaction so we have a context in which to interact with a device. This will create an asynchronous INTERACTION_STARTED event from the Relay server and your callback for that will be invoked. Now that we have an interaction context, this is where we make the Relay speak to the user. To do this we are using the sayAndWait method. The string we provide in that API method will be spoken by the Relay using its built-in text-to-speech capability. The method will return when that audio playback is done being streamed to the Relay device.

After the text-to-speech is done streaming, we are done with our interaction so we invoke the endInteraction method. This will create an asynchronous INTERACTION_ENDED event from the Relay server, and when that is received in our workflow we terminate the workflow instance with the appropriately named terminate method. With the workflow finished, the Relay server will close the websocket connection, and your workflow application will sit idle until it is triggered again.

Here's a short demo showing the 'Hello World' workflow that was created using the code and trigger above: