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.
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.
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.
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
if
componentThe 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 isTrue
. In this example thenext
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 isFalse
. In this example theflow
component is called that will start a nested flow calledflow.confirm_age
.
See the demo-app
's if.yaml flow for more interesting examples.
jump
component
jump
componentThe 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
case
componentThe 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. Ifvalue
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 thevalue
field. The action is a component reference that is called whenvalue
matches the match value. -
default
: This is the default action shouldvalue
not match any of the match values.
match
component
match
componentThe 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. Ifvalue
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 thevalue
field. The action is a component reference that is called when thevalue
matches the regex pattern. -
default
: This the default action should thevalue
not match any of the regex patterns.
cond
component
cond
componentThe 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 toTrue
.
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:
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
flow
componentThe 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 totrue
, the bot's flow control will be transferred to the nested flow, and the calling flow will be stopped. The default isfalse
. -
async
: This property tells the flow component to execute the nested flow in parallel and continue with the calling flow immediately. The default isfalse
. -
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
flow
component in actionsA 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
end
componentThe 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.
Updated almost 2 years ago