Interactions

Interactions are a context in which workflow actions run on a device. Most workflow actions that you run on a device (say, listen, LEDs, etc) will require this interaction context, specified as an argument to the SDK method for that action. Think of an interaction as a connection between a workflow and a device.

For example, when a workflow interaction is not present on a device, when you press the Talk button it will operate in the built-in walkie-talkie mode. When a workflow interaction is present on that device, it will instead send a BUTTON press event to your workflow code and not perform the walkie talkie function. So think of the interaction as letting a workflow take over the built-in behaviors of a device. Another way of thinking of it is that an interaction is a way to put the device on a special workflow channel, and the device behavior will depend on the channel.

Starting an Interaction

Most commonly, a single workflow will be associated to a single device. But it can be more complex than that. For the moment, let's stick with just a single device.

When your workflow starts and your handler for the START event gets invoked, in that START event you'll be passed the URI of the device that triggered the workflow (if the trigger came from a device). Most likely, in your START handler you'll want to start an interaction with a device, usually the device that triggered the workflow. You do that by calling the startInteraction method with the URI of the device or group that you want to interact with, along with a name that you choose for this interaction. For example:

...  
  workflow.on(Event.START, async (event) => {
    const { trigger: { args: { source_uri } } } = event
    // Start an interaction with the triggering device by calling the 'startInteraction' API.
    // For example, source_uri may look like "urn:relay-resource:name:device:Alice"
    await workflow.startInteraction([source_uri], `hello interaction`)
  })
...
...
@wf.on_start
async def start_handler(workflow, trigger):
  # Start an interaction with the triggering device by calling the 'start_interaction' API.
  # For example, target may look like "urn:relay-resource:name:device:Alice"
  target = workflow.make_target_uris(trigger)
  await workflow.start_interaction(target, 'hello interaction')
...
...
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"];
  // Start an interaction with the triggering device by calling the 'StartInteraction' API.
  // For example, deviceUri may look like "urn:relay-resource:name:device:Alice"
  Relay.StartInteraction(this, deviceUri, "hello interaction", new Dictionary<string, object>());
}
...
...
@Override
public void onStart(Relay relay, StartEvent startEvent) {
  super.onStart(relay, startEvent);

  String deviceUri = Relay.getSourceUriFromStartEvent(startEvent);
  // Start an interaction with the triggering device by calling the 'startInteraction' API.
  // For example, deviceUri may look like "urn:relay-resource:name:device:Alice"
  relay.startInteraction( deviceUri, "hello interaction", null);
}
...
...
 api.OnStart(func(startEvent sdk.StartEvent) {
   deviceUri := api.GetSourceUri(startEvent)
   // Start an interaction with the triggering device by calling the 'StartInteraction' API.
   // For example, deviceUri may look like "urn:relay-resource:name:device:Alice"
   api.StartInteraction(deviceUri, "hello interaction")
 })
...

The name you choose for the interaction (i.e., hello interaction above) also becomes the channel name of the dynamically-created channel on that device that exists while the interaction is running. This channel name won't be visible, unless the user taps the Assistant button to hear the channel name, or if the user navigates to another channel and then back, which is not common. For the most part, what you pick as the interaction name isn't a big deal, as long as it is unique from any other interaction.

You can also start an interaction by constructing your own URI instead of pulling it from the START event. This is helpful when you have a specific device in mind. For example:

...
    const deviceUri = Uri.deviceName("Alice")
    // ^ assigned "urn:relay-resource:name:device:Alice"
    await workflow.startInteraction(deviceUri, 'hello interaction')
...
...
    device_uri = relay.workflow.device_name("Alice")
    # ^ assigned "urn:relay-resource:name:device:Alice"
    await workflow.start_interaction(relay.workflow.Relay.targets_from_source_uri(device_uri), 'hello interaction')
 ...
...
            string deviceUri = RelayUri.DeviceName("Alice");
            // ^ assigned "urn:relay-resource:name:device:Alice"
            Relay.StartInteraction(this, deviceUri, "hello interaction");
 ...
...
  String deviceUri = RelayUri.deviceName("Alice");
  // ^ assigned "urn:relay-resource:name:device:Alice"
  relay.startInteraction(deviceUri, "hello interaction", null);
...
...
	deviceUri := sdk.DeviceName("Alice")
  // ^ assigned "urn:relay-resource:name:device:Alice"
  api.StartInteraction(deviceUri, "hello interaction", null)
...

You can also start the interaction against a group that you've defined in Dash. This will start an interaction on all the devices in the group. For example:

...
    const groupUri = Uri.groupName("Main")
    // ^ assigned "urn:relay-resource:name:group:Main"
    await workflow.startInteraction(groupUri, 'hello interaction')
...
...
    group_uri = relay.workflow.group_name("Main")
    # ^ assigned "urn:relay-resource:name:group:Main"
    await workflow.start_interaction(relay.workflow.Relay.targets_from_source_uri(group_uri), 'hello interaction')
 ...
...
            string groupUri = RelayUri.GroupName("Main");
            // ^ assigned "urn:relay-resource:name:group:Main"
            Relay.StartInteraction(this, groupUri, "hello interaction");
 ...
...
	String groupUri = RelayUri.groupName("Main");
  // ^ assigned "urn:relay-resource:name:group:Main"
  relay.startInteraction(groupUri, "hello interaction", null);
...
...
	groupUri := sdk.GroupName("Main")
  // ^ assigned "urn:relay-resource:name:group:Main"
  api.StartInteraction(groupUri, "hello interaction", null)
...

๐Ÿ“˜

Alike or Different User Experiences -> Same or Separate Interactions

When multiple devices are in the same interaction, that hints that all the devices should have the same user experience. If you have multiple devices in your workflow, but you want them to have different user experiences, that is a hint that the different user experiences should each be on their own separate interaction.

You can also start a single-named interaction with multiple devices across multiple invocations of startInteraction. All these devices will be addressable via that interaction name, even though you added them one at a time, as long as you use the same interaction name each time. For example:

...
    const interactionName = 'hello interaction'
    const device1Uri = Uri.deviceName("Alice")
    const device2Uri = Uri.deviceName("Bob")
    await workflow.startInteraction(device1Uri, interactionName)
    await workflow.startInteraction(device2Uri, interactionName)
...
...
    interaction_name = 'hello interaction'
    device1_uri = relay.workflow.device_name("Alice")
    device2_uri = relay.workflow.device_name("Bob")
    await workflow.start_interaction(relay.workflow.Relay.targets_from_source_uri(device1_uri), interaction_name)
    await workflow.start_interaction(relay.workflow.Relay.targets_from_source_uri(device2_uri), interaction_name)
 ...
...
            string interactionName = "hello interaction"
            string device1Uri = RelayUri.DeviceName("Alice");
            string device2Uri = RelayUri.DeviceName("Bob");
            Relay.StartInteraction(this, device1Uri, interactionName);
            Relay.StartInteraction(this, device2Uri, interactionName);
 ...
...
  String interactionName = "hello interaction";
  String device1Uri = RelayUri.deviceName("Alice");
  String device2Uri = RelayUri.deviceName("Bob");
  relay.startInteraction(device1Uri, interactionName, null);
  relay.startInteraction(device2Uri, interactionName, null);
...
...
  var interactionName string = "hello interaction"
  var device1Uri string = sdk.DeviceName("Alice")
  var device2Uri string = sdk.DeviceName("Bob")
  api.StartInteraction(device1Uri, interactionName)
  api.StartInteraction(device2Uri, interactionName)
...

๐Ÿ“˜

Unable to Start an Interaction / Workflow?

When triggering a new workflow on a device, such as a spoken phrase to the assistant, you may experience a positive confirmation beep that your phrase was successfully transcribed and mapped to a defined workflow trigger, but then immediately followed by a negative confirmation dum-dum sound. This may be because you are trying to instantiate a new workflow when one is already running on the device. So the workflow creation was rejected. To see if there already is a running workflow, use the following CLI command:

relay workflow instance list

Using an Interaction

Starting an interaction with a device is where the device moves to a dynamically created channel for the workflow, and is how the device knows to send inputs like button presses, spoken phrases, etc. to your workflow. And your workflow, after performing whatever business logic you want, can send actions to that device such as text-to-speech, LED lighting, a vibration pattern, location lookup request, etc. When calling those actions to do TTS (text-to-speech), LED, or vibrate, etc., you pass in the interaction URI to those APIs. For example, the following code is for a vibrate workflow. Note how on the sayAndWait, vibrate, and endInteraction methods that we pass in an interaction URI. But on the startInteraction method we pass in a device URI. When the workflow starts via a device trigger, the device says "This is a default vibrate pattern" and vibrates and then the workflow terminates:

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

const app = relay()

const vibrateWorkflow = createWorkflow(workflow => {
  const interactionName = 'vibrate demo'

  workflow.on(Event.START, async (event) => {
    const { trigger: { args: { source_uri: originator } } } = event   
    // Start an interaction with a device by calling the 'startInteraction API'.
    // For example, originator may look like "urn:relay-resource:name:device:Alice"
    await workflow.startInteraction([originator], interactionName)
  })

  workflow.on(Event.INTERACTION_STARTED, async ({ source_uri: interaction_uri }) => {
    // The Relay performs some logic to interact with the user 
    // For example, interaction_uri may look like "urn:relay-resource:name:interaction:vibrate%20demo?device=urn%3Arelay-resource%3Aname%3Adevice%3AAlice"
    await workflow.sayAndWait(interaction_uri, `This is a default vibrate pattern`)
    await workflow.vibrate(interaction_uri, [100, 500, 500, 500, 500, 500])
    
    // Call to end the interaction
    await workflow.endInteraction(interaction_uri)
  })

  workflow.on(Event.INTERACTION_ENDED, async() => {
    // Call to end the workflow
    await workflow.terminate()
  })
})

app.workflow(`vibratepath`, vibrateWorkflow)
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)
vibrate_workflow = relay.workflow.Workflow('vibrate workflow')
wf_server.register(vibrate_workflow, '/vibratepath')

interaction_name = 'vibrate interaction'


@vibrate_workflow.on_start
async def start_handler(workflow, trigger):
    target = workflow.make_target_uris(trigger)
    # For example, device_url may look like "urn:relay-resource:name:device:Alice"
    await workflow.start_interaction(target, interaction_name)


@vibrate_workflow.on_interaction_lifecycle
async def lifecycle_handler(workflow, itype, interaction_uri, reason):
    if itype == relay.workflow.TYPE_STARTED:
        # The Relay performs some logic to interact with the user
        # For example, interaction_uri may look like "urn:relay-resource:name:interaction:vibrate%20demo?device=urn%3Arelay-resource%3Aname%3Adevice%3AAlice"
        await workflow.say_and_wait(interaction_uri, 'This is a default vibrate pattern')
        await workflow.vibrate(interaction_uri, [100, 500, 500, 500, 500, 500])
        # Call to end the interaction
        await workflow.end_interaction(interaction_uri)
    if itype == relay.workflow.TYPE_ENDED:
        await workflow.terminate()


wf_server.start()
...
private static string interactionName = "vibrate demo";

// The workflow is started from a single button push on a device
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"];
  // For example, deviceUri may look like "urn:relay-resource:name:device:Alice"

  // Start an interaction with a device by calling the 'startInteraction API'
  Relay.StartInteraction(this, deviceUri, interactionName);
}

// The interaction is started
public override async void OnInteractionLifecycle(IDictionary<string, object> dictionary)
{
  var type = (string) dictionary["type"];

  if (type == InteractionLifecycleType.Started)
  {
    var interactionUri = (string) dictionary["source_uri"];
    // The Relay performs some logic to interact with the user.
    // For example, interaction_uri may look like "urn:relay-resource:name:interaction:vibrate%20demo?device=urn%3Arelay-resource%3Aname%3Adevice%3AAlice"
    await Relay.SayAndWait(this, interactionUri, "This is a default vibrate pattern");
    await Relay.Vibrate(this, interactionUri, [100, 500, 500, 500, 500, 500]);
    
    // Call to end the interaction
    Relay.EndInteraction(this, interactionUri);
  }
  else if (type == InteractionLifecycleType.Ended)
  {
    // Call to end the workflow
    Relay.Terminate(this);
  }
}
...
...
private static class MyWorkflow extends Workflow {

  // The workflow is started from a single button push on a device
  @Override
    public void onStart(Relay relay, StartEvent startEvent) {
    super.onStart(relay, startEvent);
    String deviceUri = Relay.getSourceUriFromStartEvent(startEvent);
    // For example, deviceUri may look like "urn:relay-resource:name:device:Alice"

    // Start an interaction with a device by calling the 'startInteraction API'
    relay.startInteraction( deviceUri, "vibrate interaction", null);
  }

  // The interaction is started
  @Override
    public void onInteractionLifecycle(Relay relay, InteractionLifecycleEvent lifecycleEvent) {
    super.onInteractionLifecycle(relay, lifecycleEvent);

    String interactionUri = (String)lifecycleEvent.sourceUri;
    if (lifecycleEvent.isTypeStarted()) {
      // The Relay performs some logic to interact with the user.
    	// For example, interaction_uri may look like "urn:relay-resource:name:interaction:vibrate%20demo?device=urn%3Arelay-resource%3Aname%3Adevice%3AAlice"
      relay.sayAndWait(interactionUri, "This is a default vibrate pattern");
      relay.vibrate(interactionUri, [100, 500, 500, 500, 500, 500]);
      
      // Call to end the interaction
      relay.endInteraction(interactionUri);
    }
    if (lifecycleEvent.isTypeEnded()) {
      // Call to end the workflow
      relay.terminate();
    }
  }
}
...
...
sdk.AddWorkflow("hellopath", func(api sdk.RelayApi) {

  // The workflow is started from a single button push on a device
  api.OnStart(func(startEvent sdk.StartEvent) {
    deviceUri := api.GetSourceUri(startEvent)
    // For example, deviceUri may look like "urn:relay-resource:name:device:Alice"

    // Start an interaction with a device by calling the 'startInteraction API'
    api.StartInteraction(deviceUri, "vibrate interaction")
  })

  // The interaction is started
  api.OnInteractionLifecycle(func(interactionLifecycleEvent sdk.InteractionLifecycleEvent) {

    if interactionLifecycleEvent.LifecycleType == "started" {
      interactionUri := interactionLifecycleEvent.SourceUri     // save the interaction id here to use in the timer callback
      // The Relay performs some logic to interact with the user.
    	// For example, interaction_uri may look like "urn:relay-resource:name:interaction:vibrate%20demo?device=urn%3Arelay-resource%3Aname%3Adevice%3AAlice"
      api.SayAndWait(interactionUri, "This is a default vibrate pattern", sdk.ENGLISH)
      api.Vibrate(interactionUri, []int64{100, 500, 500, 500, 500, 500})
      
      // Call to end the interaction
      api.EndInteraction(interactionUri)
    }

    if interactionLifecycleEvent.LifecycleType == "ended" {
      // Call to end the workflow
      api.Terminate()
    }
  })
})
...

Part of the power of this explicit interaction model is that within a single workflow instance you can have different interactions with different devices at the same time, instead of being limited to a single device via an implicit interaction. These devices with interactions don't have to be the triggering device. As another example, your workflow can perform logic that doesn't have to interact with a device, such as receiving and sending webhooks, or setting timers to fire later: these don't require user interaction. This gives you more control and more capability. And because you can have more than one interaction, that is why they need to be explicitly created.

Your workflow will likely need to look for and process interaction lifecycle events. The common interaction lifecycle events are "start" and "stop". When you request an interaction to be started, shortly thereafter your workflow will receive an INTERACTION_STARTED event (not to be confused with the plain STARTED event for the overall workflow).

If your interaction was started with a group URI, or if you added multiple devices to a single-named interaction over several invocations to startInteraction with the same interaction name, then the lifecycle handler will be invoked multiple times, one for each device. The interaction URI parameter to that handler will be specific to that device via the ?device= filter, for example "urn:relay-resource:name:interaction:hello%20interaction?device=urn%3Arelay-resource%3Aname%3Adevice%3ABob". This applies for both the "start" and "stop" interaction lifecycle events.

When looking at the target URIs that are needed as parameters on workflow API calls, be aware that different methods need different kinds of target URIs. For example, the startInteraction API takes a target URI of a device or group of devices. The APIs that affect devices such as listen, setLed, sayAndWait, etc., will take a target URI of an interaction. And some of these target URIs may have unary values, while others, depending on the API, may be able to take a multiplicity of values. The APIs should be well documented to help identify which to use. You can find that documentation in the API reference.

Almost all workflow APIs will have a required target URI parameter, whether it is an interaction URI or a device/group URI.

When you do something like await workflow.sayAndWait(interactionUri, "hello world"), then all the devices addressable by that interaction URI will speak out the phrase "hello world" from that single "say" invocation, and they will do it in parallel at about the same time (subject to the device's network connectivity). So it depends how you address the interaction URI. If you use the device specific form with the ?device=, then your action will occur only on that device. If you use the generic form without the ?device=, then it will occur on all devices that share the same interaction name.

If you want to construct an interaction URI to control the devices addressed by it, there are helper methods in the SDK for doing that, such as Uri.interactionName.

As an extra dimension, it is possible to have multiple interactions in a workflow. Some of those interactions may be with a single device, and some of those interactions may be with multiple devices. You can mix-and-match as needed. But each device should be on no more than one interaction at any point in time.

Here is what an interaction URI typically looks like, when read from the INTERACTION_STARTED event:

"urn:relay-resource:name:interaction:hello%20interaction?device=urn%3Arelay-resource%3Aname%3Adevice%3ABob"

This is in a similar format as the other URIs (device, group, etc). But it may have a filter on the end that starts with ?device=. This filter is not required. When the filter is present, and there are multiple devices in the interaction, then it will effect only the specified device.

๐Ÿšง

Picking an Interaction Name

When starting an interaction, you need to provide a name for it. This name needs to be unique from any other interaction in your workflow instance. For the most part, you can pick any name you want. The only place where this interaction becomes visible to the user is if they tap the channel button, in which case the device will announce the device name and the channel name that it is currently on.

However, because the interaction name becomes the channel name, the interaction name cannot conflict with any other potential channel name. This means you need to avoid existing group names that have been defined in Dash, since that group is a channel.

Ending an Interaction

For every interaction that you start, you should also explicitly end it. The endInteraction method will do that, and it takes the interaction URI as a parameter. (Technically, a terminate should end any existing interactions, but it is recommended to try to cleanly end them where possible.) This will help clean up any still-running actions and tear down the dynamically-created channel.

One option is to save the interaction URI when you get it in the INTERACTION_STARTED event handler, so you can use it later for endInteraction. An easier solution is to use a known interaction name, and you can create the interaction URI on demand. For example:

...
  workflow.on(Event.BUTTON, async() => {
    const interactionUri = Uri.interactionName(interactionName)
    await workflow.endInteraction(interactionUri)
  })
...
...
@hello_workflow.on_button
async def button_handler(workflow:relay.workflow.Workflow, button:str, taps:str, source_uri:str):
    interaction_uri = relay.workflow.interaction_name(interaction_name)
    await workflow.end_interaction(interaction_uri)
...
...
public override void OnButton(IDictionary<string, object> dictionary) {
  string interactionUri = RelayUri.InteractionName(interactionName);
  Relay.EndInteraction(this, interactionUri);
}
...
...
@Override
  public void onButton(Relay relay, ButtonEvent buttonEvent) {
    String interactionUri = RelayUri.interactionName(interactionName);
    relay.endInteraction(interactionUri);
  }
...
...
  api.OnButton(func(startEvent sdk.StartEvent) {
    interactionUri := api.GetSourceUri(startEvent)
    api.EndInteraction(interactionUri)
  })
...

In the example above, this will end the interaction for all devices in the interaction. If you make a call to endInteraction with an interaction URI that has a filter with ?device= then the interaction for only that device will end.

In the full examples on this page, notice how we call endInteraction when then flows into the interaction lifecycle handler with a "stop" event, which in turns calls terminate. That is a suggested best practice.

Options

You can pass in options when starting an interaction. These options can change the LED color from the default of blue, identify the input types that you care about, and set the channel to return to after the interaction is complete. To do this, you would include a third parameter in the startInteraction() function. For instance if I started an interaction and wanted the color of the LEDs to change from the default to white, I would start it like the following:

await workflow.startInteraction(source_uri, 'my interaction', {'color': "FFFFFF"})

An Example Interaction Workflow

As you get more familiar with how to use Interactions on your Relay devices, you can grow your workflows to become more and more complex. For reference, the following is an example of a simple workflow has muti-step interaction with the user on how to make a coffee:

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

const app = relay()

const coffeeWorkflow = createWorkflow(workflow => {
  const interactionName = 'coffee interaction'
  
  workflow.on(Event.START, async(event) => {
    // The event is triggered and an interaction is started
    const { trigger: { args: { source_uri } } } = event
    workflow.startInteraction([source_uri], interactionName)
  })

  workflow.on(Event.INTERACTION_STARTED, async({source_uri: interaction_uri}) => {
    // Gets the name of the device that triggered the workflow to greet the user
    const deviceName = Uri.parseDeviceName(interaction_uri)
    await workflow.say(interaction_uri, `Getting recipies for ${deviceName}`)

    // Asks which drink the user needs a recipe for
    await workflow.say(interaction_uri, `Which drink would you like help with?`)
    const drink = await workflow.listen(interaction_uri, ['Latte','Frappuccino'])

    // The relay gives the recipe for the selected drink based on user input
    await workflow.say(interaction_uri, `Preparing the recipe for a ${drink.text}`)
    
    // The user taps the talk button once when this step is complete
    if (drink.text === 'frappuccino') {
      await workflow.say(interaction_uri, 'Blend ice, 10 ounces Milk, and a shot of espresso. Single tap when complete')
    }
    else if (drink.text === 'latte') {
      await workflow.say(interaction_uri, 'Add a shot of espresso and 10 ounces of steamed Milk.  Single tap when complete')
    } 
  })

  workflow.on(Event.BUTTON, async ({ source_uri: interaction_uri }) => {
    // The device receives a button tap indicating that the previous step is complete
    await workflow.say(interaction_uri, 'Next, top with whipped cream and caramel drizzle.  Recipe complete!')
    
    // The interaction is ended and then terminated
    await workflow.endInteraction(interaction_uri)
  })
  
  workflow.on(Event.INTERACTION_ENDED, async() => {
    await workflow.terminate()
  })
})

app.workflow('coffeepath', coffeeWorkflow)
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)
coffee_workflow = relay.workflow.Workflow('coffee workflow')
wf_server.register(coffee_workflow, '/coffeepath')

interaction_name = 'coffee interaction'


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


@coffee_workflow.on_interaction_lifecycle
async def lifecycle_handler(workflow, itype, interaction_uri, reason):
    if itype == relay.workflow.TYPE_STARTED:
        # Gets the name of the device that triggered the workflow to greet the user
        device_name = await workflow.get_device_name(interaction_uri)
        await workflow.say_and_wait(interaction_uri, f'Getting recipes for {device_name}')

        # Asks which drink the user needs a recipe for
        await workflow.say(interaction_uri, 'Which drink would you like help with')
        drink = await workflow.listen(interaction_uri, ['latte', 'frappuccino'])

        # The relay gives the recipe for the selected drink based on user input
        await workflow.say(interaction_uri, f'Preparing the recipe for a {drink}')

        # The user taps the talk button once when this step is complete
        if drink == 'frappuccino':
            await workflow.say(interaction_uri, 'Blend ice, 10 ounces milk, and a shot of espresso.  Single tap when complete')

        elif drink == 'latte':
            await workflow.say(interaction_uri, 'Add a shot of espresso and 10 ounces of steamed milk.  Single tap when complete')
    if itype == relay.workflow.TYPE_ENDED:
        await workflow.terminate()


@coffee_workflow.on_button
async def button_handler(workflow, button, taps, interaction_uri):
    # The device receives a button tap indicating that the previous step is complete
    await workflow.say(interaction_uri, "Next top with whipped cream and caramel drizzle.  Recipe complete!")

    # The interaction is ended and then terminated
    await workflow.end_interaction(interaction_uri)


wf_server.start()
using System.Collections.Generic;
using RelayDotNet;

namespace SamplesLibrary
{
  public class Coffee : AbstractRelayWorkflow
  {
    public Coffee(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"];
      // The event is triggered and an interaction is started
      Relay.StartInteraction(this, deviceUri, "coffee", 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"];
        
        // Gets the name of the device that triggered the workflow to greet the user
        var deviceName = await Relay.GetDeviceName(this, interactionUri);
        await Relay.Say(this, interactionUri, $"Getting recipes for {deviceName}");
        
        // Asks which drink the user needs a recipe for
        await Relay.Say(this, interactionUri, "Which drink would you like help with");
        string[] drinks = {"latte", "frappuccino"};
        var drink = await Relay.Listen(this, interactionUri, drinks);

        // The relay gives the recipe for the selected drink based on user input
        await Relay.Say(this, interactionUri, $"Preparing recipe for a {drink["text"]}");
    
        // The user taps the talk button once when this step is complete
        if(drink == "frappuccino")
        {
          await Relay.Say(this, interactionUri, "Blend ice, 10 ounces of milk, and a shot of espresso.  Single tap whenc complete"); 
        }
        else if (drink == "latte")
        {
          await Relay.Say(this, interactionUri, "Add a shot of espresso and 10 ounces of steamed milk.  Single tap when complete");
        }

      }
      else if (type == InteractionLifecycleType.Ended)
      {
        Relay.Terminate(this);
      }
    }

    public override async void OnButton(IDictionary<string, object> dictionary)
    {
      string sourceUri = (string) dictionary["source_uri"];
      
      // The device receives a button tap indicating that the previous step is complete
      await Relay.Say(this, sourceUri, "Next top with whipped cream and caramel drizzle.  Recipe complete!");
      
      // The interaction is ended and then terminated
      Relay.EndInteraction(this, sourceUri);
    }
  }
}
package com.relaypro.app.examples.standalone;

import com.relaypro.app.examples.util.JettyWebsocketServer;
...
import java.util.Map;

public class Coffee {

    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("coffeepath", 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);
            // The event is triggered and an interaction is started
            relay.startInteraction( sourceUri, "coffee", null);
        }

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

            String interactionUri = (String)lifecycleEvent.sourceUri;
            // Gets the name of the device that triggered the workflow to greet the user
            String deviceName = relay.getDeviceName(interactionUri, false);
            if (lifecycleEvent.isTypeStarted()) {
                relay.say(interactionUri, "Getting recipes for "  + deviceName);
								
                // Asks which drink the user needs a recipe for
                relay.say(interactionUri, "Which drink would you like help with");
                String[] drinks = {"latte", "frappuccino"};
                String drink = relay.listen(interactionUri, "request-1", drinks, false, LanguageType.English, 10);
                
              	// The relay gives the recipe for the selected drink based on user input  
                relay.say(interactionUri, "Preparing recipe for " + drink);
                
              	// The user taps the talk button once when this step is complete
              	if (drink.equals("frappuccino")) {
                    relay.say(interactionUri, "Blend ice, 10 ounces of milk, and a shot of espresso.  Single tap whenc complete");
                }
                else if (drink.equals("latte")) {
                    relay.say(interactionUri, "Add a shot of espresso and 10 ounces of steamed milk.  Single tap when complete");
                }
            }
            if (lifecycleEvent.isTypeEnded()) {
                relay.terminate();
            }
        }

        @Override
        public void onButton(Relay relay, ButtonEvent buttonEvent) {
            super.onButton(relay, buttonEvent);
            String sourceUri = (String)buttonEvent.sourceUri;
          	// The device receives a button tap indicating that the previous step is complete
            relay.say(sourceUri, "Next top with whipped cream and caramel drizzle.  Recipe complete!");
            
          	// The interaction is ended and then terminated
            relay.endInteraction(sourceUri);
        }
    }
}
package main

import (
    "relay-go/pkg/sdk"
)

var port = ":8080"

func main() {

    sdk.AddWorkflow("coffeepath", func(api sdk.RelayApi) {
        
        api.OnStart(func(startEvent sdk.StartEvent) {
            sourceUri := api.GetSourceUri(startEvent)
            // The event is triggered and an interaction is started
            api.StartInteraction(sourceUri, "coffee")
        })
        
        api.OnInteractionLifecycle(func(interactionLifecycleEvent sdk.InteractionLifecycleEvent) {
            interactionUri := interactionLifecycleEvent.SourceUri
            // Gets the name of the device that triggered the workflow to greet the user
            deviceName := api.GetDeviceName(interactionUri, false)
            if interactionLifecycleEvent.LifecycleType == "started" {
                api.Say(interactionUri, "Getting recipes for " + deviceName, sdk.ENGLISH)
                
              	// Asks which drink the user needs a recipe for
                api.Say(interactionUri, "Which drink would you like help with?", sdk.ENGLISH)
                drink := api.Listen(interactionUri, []string{"latte", "frappuccino"}, false, sdk.ENGLISH, 10)
              	
              	// The relay gives the recipe for the selected drink based on user input  
                api.Say(interactionUri, "Preparing recipe for " + drink, sdk.ENGLISH)

                // The user taps the talk button once when this step is complete
                if drink == "frappuccino" {
                    api.Say(interactionUri, "Blend ice, 10 ounces of milk, and a shot of espresso.  Single tap whenc complete", sdk.ENGLISH)
                } else if drink == "latte" {
                    api.Say(interactionUri, "Add a shot of espresso and 10 ounces of steamed milk.  Single tap when complete", sdk.ENGLISH)
                }
            }

            if interactionLifecycleEvent.LifecycleType == "ended" {
                api.Terminate()
            }
        })

        api.OnButton(func(buttonEvent sdk.ButtonEvent) {
            sourceUri := buttonEvent.SourceUri
            // The device receives a button tap indicating that the previous step is complete
            api.Say(sourceUri, "Next top with whipped cream and caramel drizzle.  Recipe complete!", sdk.ENGLISH)

            // The interaction is ended and then terminated
            api.EndInteraction(sourceUri)
        })
    })
    
    sdk.InitializeRelaySdk(port)
}

This workflow utilizes the different ways to use the say(), sayAndWait(), and listen() functions. You can find more information on how to implement these functions and what they do in the Say and Listen section on the next page.