Flows

Express your conversational UX using flows written in BFML

Flows are made up of steps, or in other words, a series of component elements that are executed in order.

Flows are usually stored under the flow/ folder in your app's repo.

Below is an example of a flow:

steps:
  - (start)
  - type: component.order.get
    order_id: (@ flow.order_id )
  - thread_set: order
  - type: component.product.get
    product_id: (@ thread.order.product_ids[0] )
  - thread_set: product
  - ask_form: Why would you like to return this item?
    label: Please specify reason
    composer:
      visibility: hide
  - flow_set: reason
 
  - (order_debug)
  - say: >
      Thanks. Before we can approve your return, I need to bring a human to help.
      One moment, while I get someone...
  - flow: flow.escalate.ticket
    data:
      subject: '♻️ Return: Order #(@ thread.order.id ) - (@ thread.product.name )'
      comment: |
        order #: (@ thread.order.id )
        status: (@ thread.order.status )
        product: (@ thread.product.name )
        price: (@ "${:,.0f}".format(thread.product.price) )
        reason: (@ flow.reason )

From this snippet you can see that each step defines a specific action that the bot performs. Each component takes in a number of configuration properties for example: label, composer, order_id etc. The app runtime will then evaluate these properties and then execute the component's associated Python class that contains all the execution logic.

The Meya SDK provides many different types of built-in components that can be used to enable the bot to perform complex tasks.

Triggers and flows

Unless you are implementing a nested flow, you usually need to associate a flow with a set of triggers that will trigger and run the flow.

Here is complete example of a flow with it's associated triggers, in this case the flow get run whenever the user types either order_return or order debug:

triggers:
  - keyword: order_return
    action:
      jump: start
      data:
        order_id: o-4
  - regex: order.*debug
    action:
      jump: order_debug

steps:
  - (start)
  - type: component.order.get
    order_id: (@ flow.order_id )
  - thread_set: order
  - type: component.product.get
    product_id: (@ thread.order.product_ids[0] )
  - thread_set: product
  - ask_form: Why would you like to return this item?
    label: Please specify reason
    composer:
      visibility: hide
  - flow_set: reason
 
  - (order_debug)
  - say: >
      Thanks. Before we can approve your return, I need to bring a human to help.
      One moment, while I get someone...
  - flow: flow.escalate.ticket
    data:
      subject: '♻️ Return: Order #(@ thread.order.id ) - (@ thread.product.name )'
      comment: |
        order #: (@ thread.order.id )
        status: (@ thread.order.status )
        product: (@ thread.product.name )
        price: (@ "${:,.0f}".format(thread.product.price) )
        reason: (@ flow.reason )

Implementing flows

This quickest, and simplest way to implement or change a flow file is to use the Meya Console. The Meya Console provides a simple flow text editor as well as a visual flow editor.

It is often useful to switch between text and visual flow editing modes when building out new flows. Most bot developers, however, prefer to use the text editor because it's often quicker to make large changes, but they will also use the visual flow editor to help visualize the flow branching in complex flows.

2582

An example of editing a flow in the Meya Console text editor.

The visual flow editor also helps non-technical users visualize the flow logic and allows them to make simple flow changes without having to understand BFML syntax.

2584

An example of the same flow in the Meya Console visual flow editor.

Using a Python IDE and Meya CLI

The Meya Console is convenient to make quick flow changes, however, most developers prefer to use a fully featured Python IDE (Integrated Development Environment) to develop an advanced bot.

Using a Python IDE is especially useful when implementing custom elements in Python e.g. custom components, triggers or integrations. Meya provides all the required Python source code for the Meya SDK that enables a bot developer to easily implement custom elements with full Python code completion.

2770

An example of editing the same flow file in the PyCharm IDE.

When developing a Meya bot using a Python IDE, you will also need to setup the Meya CLI to be able to push code changes to your app running on the Meya platform.

You are free to use any Python IDE or local editor of your choice, however, we strongly recommend using either PyCharm or Visual Studio Code, as both these IDEs have excellent Python support.

Flow reference paths

A flow's reference path is simply the flow's file path where the slashes / are replaced with dots ., and the file extension is dropped.

(If you're familiar with Python, a flow reference path is similar to a Python file's module path.)

Here are some example reference paths:

  • flow/faq/answers/component.yaml becomes: flow.faq.answers.component
  • flow/routing.yaml becomes: flow.routing

Branching

In Meya there are a couple of components that enable branching the execution of the flow based on certain criteria.


if component

The if component is the most common component used for branching a flow (this is similar to an if statement in a conventional programming language). Here is an example:

- if: (@ user.age > 18)
  then: next
  else:
    flow: flow.confirm_age
  • if: Contains the evaluation criteria which is expressed using Meya's template syntax.

  • then: Contains the action component reference that is called when the evaluation criteria is True. In this example the next component is called and the flow will proceed to the next step.

  • else: Contains the action component reference that is called when the evaluation criteria is False. In this example the flow component is called that will start a nested flow called flow.confirm_age.

See the demo-app's if.yaml flow for more interesting examples.


jump component

The jump component allows you to jump to a specific label step in your flow. A label step has the following syntax:

- (label name)

Here is an example of a jump component:

- jump: some_label

Here is a complete example:

steps:
  - say: Hi
  - jump: second_label

  - (first_label)
  - say: You reached `first_label`
  - end

  - (second_label)
  - say: You reached `second_label`
  - end

In this example, the flow will jump from the second step to the step containing the second_label label and continue executing the flow from there.

  • jump: The label to jump to.

  • data: Flow scope data to set before jumping to the label.


case component

The case component is a more advanced branching component that allows you to match against multiple match values (this is similar to a switch statement in Javascript/Java/C/C++). Here is an example:

- value: (@ user.gender ) 
  case:
    male:
      jump: male
    female:
      jump: female
   default:
      jump: other
  • value: Sets the value that needs to be matched against the multiple match values. If value is not defined, then the component will try use the value set in (@ flow.result ), if no value could be found then an error is raised.

  • case: Contains a set of match values and actions that will be matched against the value defined in the value field. The action is a component reference that is called when value matches the match value.

  • default: This is the default action should value not match any of the match values.


match component

The match component allows you to match against multiple regex (regular expression) patterns. Here is an example:

# Match a/b/c/d
- value: (@ flow.foo )
  match:
    (?P<a_group>a+):
      say: A (@ flow.groups.a_group )
    b.b:
      jump: b
    (cc)+:
      jump: c
  default:
    jump: d
  • value: Sets the value that needs to be matched against the multiple regex patterns. If value is not defined, then the component will try use the value set in (@ flow.result ), if no value could be found then an error is raised.

  • match: Contains a set of regex patterns and actions that will be evaluated against the value defined in the value field. The action is a component reference that is called when the value matches the regex pattern.

  • default: This the default action should the value not match any of the regex patterns.


cond component

The cond component is another branching component that allows you to specify multiple evaluation criteria and associated actions. Here is an example:

- cond:
    - (@ user.age < 18):
        flow: flow.confirm_age 
    - (@ user.age >= 18 and user.age < 65 ):
        jump: next
    - (@ user.age >= 65):
        flow: flow.retired 
  default: next
  • cond: Contains a set of evaluation criteria that will be evaluated in order starting with the first evaluation criteria. The evaluation criteria is expressed using Meya's template syntax.

  • default: This is the default action should none of the evaluation criteria evaluate to True.

Nested Flows

In Meya a flow can run other flows from a flow step using the special flow component. This is very useful if you have common functionality that you would like to use in a number of flows. For example, if you need to capture the user's name and email address in a number of flows, it will be simpler to create a specific flow to capture these details and then call this common flow from other flows using the flow's reference path.

Here is an example of a flow calling another flow:

triggers:
  - keyword: agent

steps:
  - say: I'll need to get your details first.
  - flow: flow.get_user_info
  - say: Great! Thank you for you info (@ user.name )
  - say: I'll now transfer you to a human agent.
  ...

Flow file stored in flow/get_user_info.yaml

steps:
  - (name)
  - ask: What is your name?
  - user_set: name

  - (email)
  - ask: What is your email address?
  - user_set: email

In the above example the first flow calls the flow reference path flow.get_user_info on the second step. The first flow will then pause and wait for the nested flow, flow.get_user_info, to complete before continuing on to step three.

Flow call stack

When executing nested flows, the BFML runtime maintains a flow call stack to keep track of all the parent flows, and where to continue execution from when a nested flow ends.

So for example, in the first flow above, flow.get_user_info gets called on step 2, this flow then gets pushed onto the flow call stack keeping track that the nested flow was called at step 2. When the flow.get_user_info flow ends, then the first flow will be read (or popped) from the stack, and the first will continue executing step 3 until it ends.

This pattern can be repeated many times with multiple nested flows, but there is an upper limit of the number of nested flows that can be called until the BFML runtime will throw a stack overflow error.

Here is an example of what a flow call stack in the flow.entry.start entry looks like:

2726

Logs & GridQL view of a flow call stack in the tracked in the flow.entry.start entry.

Transferring flow control

There are times when you might not want the flow execution to return to the calling (or parent) flow when the nested flow ends. In this case we would like to transfer the flow execution to the nested flow.

This is achieved by setting the transfer: true field in the flow component:

- flow.get_user_info
  transfer: true

Note, transferring flow execution to a nested flow will result in the calling (or parent) flow not being tracked in the flow call stack.

Parallel flows

You can create a parallel flow by setting the async: true field in the flow component. This will call the nested flow, but it will create a new flow execution thread and run the nested flow in parallel to the calling flow. The calling flow will not wait for the nested flow, but will immediately continue running flow's next step.


flow component

The flow component takes in the reference path of another flow that it should run at a particular flow step. (Running nested flows is very analogous to calling functions with in a conventional programming language such as Python).

- flow: flow.get_user_info
  transfer: true
  async: false
  jump: email
  data:
     user_id: (@ flow.user_id )
     foo: bar
  • flow: This is the reference path to the flow that needs to be called.

  • transfer: The property tells the calling flow whether or not to continue with the flow once the nested flow is complete. If set to true, the bot's flow control will be transferred to the nested flow, and the calling flow will be stopped. The default is false.

  • async: This property tells the flow component to execute the nested flow in parallel and continue with the calling flow immediately. The default is false.

  • jump: This property tells the flow component to jump to a specific label step in the nested flow. By default, a nested flow will always start execution from the first step.

  • data: This property allows you to pass any flow scope variables from the calling flow to the nested flow. (This is analogous to passing function parameters in a conventional programming language such as Python).

  • bot: The is the reference path to the bot that you would like to run the nested flow for. In Meya you can configure multiple bots per app and then run flows as different bots. When running a flow as another bot, any bot events e.g. text.say, will be attributed to that bot with that bot's name and avatar. By default this property always assumes the primary/default bot if not specified.

  • thread_id: This property tells the flow component to execute the nested flow on another conversation thread. This generally used for advanced use cases where a bot needs to manage multiple conversation threads.

Using the flow component in actions

A common pattern is to use the flow component in actions defined in other elements such as triggers, buttons, quick replies and branching components. Here is an example of a quick reply calling a nested flow when clicked:

steps:
  - say: What would you like to do next?
    quick_replies:
      - text: Talk to an agent
        action:
          flow: flow.agent_transfer
          transfer: true
          data: (@ flow )
      - text: Search for articles
        action:
          jump: article search

end component

The end component allows you to end the flow at any step, and optionally return data to a calling flow.

steps:
  - (option 1)
  - say: This is option 1
  - end:
      selected_option: 1

  - (option 2)
  - say: This is option 2
  - end:
      selected_option: 2

In this example the flow will end without processing the steps under option 2, when option 1 is executed. If this flow was called by another flow, then the selected_option variable will be available in the calling flow's flow scope under (@ flow.selected_option ).


Loops

Using the combination of step labels and the jump component, it is very simple to create a loop in the flow.

Here is an example flow that implements a loop and uses a flow scope variable called (@ flow.sum ) to output the sum of a user's input, and if the user types stop, the loop will stop and end the flow:

steps:
  - ask: Start at...
  - flow_set: sum

  - (print)
  - say: Sum is (@ flow.sum )
  - ask: Plus...
  - flow_set: plus
  - jump: >
      (% if flow.plus == "stop" %)
        stop
      (% else %)
        continue
      (% endif %)
  - (continue)
  - flow_set:
      sum: (@ flow.sum + flow.plus )
  - jump: print

  - (stop)
  - say: Stopped

Note, because the user's input is text, the sum will contain the concatenation of all the user's input instead of the mathematical sum πŸ€“

Recursion

It is also possible to implement recursive loops by using the flow component and calling the current flow as a nested flow.

Here is the same example from above but using recursion instead:

steps:
  - ask: Start at...
  - flow_set: sum

  - (print)
  - say: Sum is (@ flow.sum )
  - ask: Plus...
  - flow_set: plus
  - jump: >
      (% if flow.plus == "stop" %)
        stop
      (% else %)
        continue
      (% endif %)
  - (continue)
  - flow: flow.loop
    jump: print
    data:
      sum: (@ flow.sum + flow.plus )
    transfer: true
  - say: Flow control error

  - (stop)
  - say: Stopped

Instead of using a jump near step 8, we use the flow component instead and call the flow itself.

Note, by setting transfer: true we transfer flow control to the nested flow and therefore do not need to maintain the nested flow call on the flow call stack, so there is no risk of a stack overflow πŸ˜€πŸ™ŒπŸ»

However, often you would want to main the call stack when implementing a recursive algorithm that leverages the return result of the flow.

See the demo-app's factorial.yaml flow to see an implementation of a recursive algorithm.