Per the previous section, a trigger is what causes your workflow to get instantiated and running. Once your workflow is running, the Relay server will send events to your workflow. You need to write code to handle the events you are interested in, so you can react to them. Most of your workflow should be written as event-driven code. From your event-driven code, you can send actions to the Relay server for it to forward to devices for them to act on, or for the Relay server itself to act on.

When events are fired, many event types will have parameters that accompany them. For example, when there is an INTERACTION_LIFECYCLE_EVENT sent to your workflow, the interaction URN will be passed in with it.

Common Events

START: this acts as the entry point for your workflow when it gets started by a trigger. The trigger will cause a START_EVENT to get fired and sent to your workflow instance, which should arrive shortly after your workflow is instantiated. This is most likely the first part of your workflow code to run. One of the things you are very likely to do in your START_EVENT handler is to create an interaction, which in turn will cause a START_INTERACTION_EVENT to occur (see below).

INTERACTION_STARTED: this event occurs when you request a new interaction to be started, which you do explicitly via the startInteraction method. Per above, you probably requested a new interaction so that you can send actions to a devices (this creates a context for you to interact with a device). The creation of the interaction after the request to do so is asynchronous instead of blocking. Almost all the actions you want to perform on a device will require an interaction. An interaction URN is passed to you with this event, which gives you the handle to interact with devices via actions. Once you receive this interaction URN, you can start interacting with a device via actions.

INTERACTION_ENDED: an interaction has stopped. Typically you'll stop an interaction yourself explicitly via the stopInteraction method, which will in turn fire this event asynchronously. If you are done with all your interactions and workflow processing, in this handler you can call terminate to end your workflow instance, and the websocket connection will automatically close.

INTERACTION_LIFECYCLE_EVENT: a generic version of INTERACTION_STARTED and INTERACTION_ENDED. In addition to "start" and "end", this may also include "resume" and "suspend".

PROMPT: when a text-to-speech (generated voice) is being streamed to a Relay device, this event will mark the beginning and end of that stream delivery. Typically this is informational-only, and you shouldn't need to write a handler for this event. However, if you used the non-blocking say instead of the blocking sayAndWait, then you may need to look for this event to know when the text-to-speech is done streaming to a device before you continue on with something else.

BUTTON: when a button is pressed during a running workflow. Note that this is separate from when a button is pressed to trigger a workflow. This is a way to do complex user interactions in a workflow.

NOTIFICATION: when a broadcast or alert is happening on the device.

STOP: this indicates that the workflow has stopped, which might be due to a normal completion after you call terminate, or an abnormal completion upon an error. You don't need to have a handler for this, unless you want to do some logging.

Now that you are more familiar with what these events mean, the following code should start to make sense:

import { relay, Event, createWorkflow, Uri } from '@relaypro/sdk'

const app = relay({port: 8080})

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: interaction_uri }) => {
    await workflow.sayAndWait(interaction_uri, 'hello world')
    await workflow.endInteraction(interaction_uri)

  workflow.on(Event.INTERACTION_ENDED, async() => {
    await workflow.terminate()

app.workflow(`hellopath`, helloWorkflow)
import relay.workflow
import os

port = os.getenv('PORT', 8080)
wf_server = relay.workflow.Server('', port)
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)

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.Say(interactionUri, "Hello world", sdk.ENGLISH)

      if interactionLifecycleEvent.LifecycleType == "ended" {


Less Common Events

SPEECH: when the listen() function is happening on a device. Typically this is informational-only, and you shouldn't need to write a handler for this event.

TIMER_FIRED: a named timer has fired.

INCIDENT: an incident has been resolved. Typically this is informational-only, and you shouldn't need to write a handler for this event.

PROGRESS: in the case when a long-running action is being performed across a large number of devices (i.e., a group), each device will be processed serially by the Relay server, and this event will be fired after every 15 devices are processed, so that during this long-running task you can see that it is continuing to make progress. If in your workflow code you are doing any kind of timeout waiting for a long-running event to complete, this typically would reset your timeout.