Version: Latest

Business Logic with Flows

Flows in Rasa offer a structured way to design conversation-driven business logic. This page details the rules for effectively using and managing flows within Rasa.

New in 3.7

Flows are part of Rasa's new Conversational AI with Language Models (CALM) approach and available starting with version 3.7.0.

Overview

In CALM, the business logic of your AI assistant is implemented as a set of flows. Each flow describes the logical steps your AI assistant uses to complete a task. It describes the information you need from the user, data you need to retrieve from an API or a database, and branching logic based on the information collected.

A flow in Rasa only describes the logic your assistant follows, not all the potential paths conversations can take. If you're used to designing AI assistants by creating flow charts of how conversations should go, you'll see that flows in Rasa are much simpler. Check out Rasa Studio's Flow Builder to build flows with a web interface.

To get familiar with how flows work, follow the tutorial. This page provides a reference of the format and properties of flows.

Hello World

A Flow is defined using YAML syntax. Here is an example of a simple flow:

flows.yml
flows:
hello_world:
description: "A simple flow that greets the user"
steps:
- action: "utter_greet"

Flow Properties

A flow is defined using the following properties:

flows.yml
flows:
a_flow: # required id
name: "A flow" # optional name
description: "required description of what the flow does"
always_include_in_prompt: false # optional boolean, defaults to false
if: "condition" # optional flow guard
nlu_trigger: # optional list of intents that can start a flow
- intent: "starting_flow_intent"
steps: [] # required list of steps

Name

The name field is an optional human readable name for the flow.

Description

The description field is a summary of the flow. It is required, and should describe what the flow does for the user. Writing a clear description is important, because it is used by the Dialogue Understanding component to decide when to start this flow. See Starting Flows for more details. Additionally, for guidelines on how to write concise and clear descriptions see the section provided here.

Always Include in Prompt

If always_include_in_prompt field is set to true and the flow guard defined in the if field evaluates to true, the flow will be always be included in the prompt

NLU Trigger property

The nlu_trigger field is used to add intents that can start the flow.

If property

The if field is used to add flow guards.

Steps

The steps field is required and lists the flow steps. There are six types of steps:

  • action step: a custom action or utterance action run by the flow
  • collect step: a question asked to the user to fill a slot
  • call step: a step to call another flow
  • link step: a step to link another flow after this flow is finished
  • set slots step: a step to set slots
  • noop step: a step to create conditions without necessarily running an action, uttering a response, collecting a slot or linking / calling another flow in that step itself.

All steps share these common properties:

- id: "an_optional_unique_id"
description: "an optional description, used to help dialogue understanding extract slots correctly"
next: "an id or conditions to determine which step should be executed next"

Id Property

The id field is an optional unique identifier for each step. It is used to reference the step in the next field of other steps.

Next Property

The next field specifies which step is executed after this one. If it is omitted, the following step in the list is the next one.

To link a specific step, you can use the next step's id:

- collect: "name"
next: "the_next_step"
- id: "the_next_step"
collect: "age"

You can also create a nested structure by listing steps under the next field:

- collect: "name"
next:
- collect: "age"

This is sometimes easier to read, especially when using conditions to create branching logic. For example:

- collect: "age"
next:
- if: slots.age < 18
then:
- action: utter_not_old_enough
next: END
- if: slots.age >= 18 and slots.age < 65
then: "18_to_65_step"
- else: "over_65_step"

Depending on the user's input value for age, this example show the flow proceeding to different steps. Note the age slot must be prefixed by the slots. namespace to be accessible in the condition. For namespaces, navigate to the Namespaces section.

When a condition is met that leads to next: END, the flow will complete without executing further steps.

Step Types

Every step in a flow has a type. The type determines what the step does and what properties it supports. A step must have exactly one of they following keys:

  • action
  • collect
  • link
  • call
  • set_slots
  • noop

Action

A step with the key action: action_do_something instructs your assistant to execute action_do_something and then proceed to the next step. It will not wait for user input.

The value of the action key is either the name of a custom action:

- action: "action_check_sufficient_funds"

Or the name of a response defined in your domain:

- action: "utter_insufficient_funds"

Collect

A step with a collect key instructs your assistant to request information from the user to fill a slot. For example:

- collect: "account_type" # slot name

Your assistant will not proceed to the next step in the flow until the slot account_type has been filled. See the tutorial for more information about slot filling and defining slots in your domain file.

Always Asking Questions

By default, a collect step is skipped if the corresponding slot is already filled. If a collect step should always be asked, no matter if the underlying slot is already filled, you can set ask_before_filling: true on the collect step:

- collect: "final_confirmation" # slot name
ask_before_filling: true

If the "final_confirmation" slot is already filled, the assistant will clear it and proceed to ask the collect step, then fill the slot again. This ensures the confirmation collect step is always asked.

Resetting slots at the End of a Flow

By default, all slots filled via collect steps are reset when a flow completes. Once the flow ends, the slot's value is either reset to null or to the slot's initial value (if provided in the domain file under slots).

If you want to retain the value of a slot after the flow completes, set the reset_after_flow_ends property to false. For example:

- collect: "user_name"
reset_after_flow_ends: false

Using a different response key for the collect step

By default, Rasa will look for a response called utter_ask_{slot_name} to execute a collect step. You can use a response with a different key by adding an utter property to the step.

For example:

flows.yml
flows:
replace_supplementary_card:
description: This flow helps the user to replace a supplementary card for a family member.
steps:
- collect: account_type
utter: utter_ask_secondary_account_type
next: "ask_account_number"

Using an action to ask for information in collect step

Instead of using a templated response for the collect step you can also use a custom action. This is useful if you, for example, want to display available values for the slot fetched from a database as buttons. Rasa will look for an action called action_ask_{slot_name} to execute a collect step. The custom action needs to strictly follow the specified naming convention. Make sure to add the custom action action_ask_{slot_name} to your domain file.

domain.yml
actions:
- action_ask_{slot_name}

You don't need to update the collect step itself to use the custom action. The collect step inside the flow stays the same, for example,

- collect: {slot_name}
info

You can define either a response or a custom action for your collect step. It is not allowed to define both. A validation error will be thrown by Rasa if both are defined.

Slot validation

You can define slot validation rules directly in your flow yaml file by adding a rejections property to any collect step. The rejections section is a list of mappings. Each mapping must have if and utter mandatory properties:

  • the if property is a condition written in natural language and evaluated using the pypred library. In the condition, you can only use the slot name that is collected in this step. At the moment, using other slots is not supported.
  • the utter property is the name of the response the assistant will send if the condition evaluates to True.

Here is an example:

flows.yml
flows:
verify_eligibility:
description: This flow verifies if the user is eligible for creating an account.
steps:
- collect: "age"
rejections:
- if: slots.age < 1
utter: utter_invalid_age
- if: slots.age < 18
utter: utter_age_at_least_18
next: "ask_email"

When validation fails, your assistant will automatically try to collect the information again. The assistant will repeatedly ask for the slot until the value is not rejected.

If the predicate defined in the if property is invalid, the assistant will log an error and respond with the utter_internal_error_rasa response which by default is Sorry, I'm having trouble understanding you right now. Please try again later.

You can override the text message for utter_internal_error_rasa by adding this response with the custom text in your domain file.

note

For more complex validation logic, you can also define slot validation in a custom action. Note this custom action must follow this naming convention: validate_{slot_name}.

Link

A link step is used to connect flows. Links can only be used as the last step in a flow. When a link step is reached, the current flow is ended and the targeted flow is started. You can define a link step as:

- link: "id_of_another_flow"

It is not possible to add any other property like an action or a next to a link step. Doing so, will result in an error during flow validation.

Call

New in 3.8.0

The call step is available starting with version 3.8.0.

You can use a call step inside a flow (parent flow) to embed another flow (child flow). When the execution reaches a call step, CALM starts the child flow. Once the child flow is complete, the execution continues with the parent flow.

Calling other flows helps split up and reuse functionality. It allows you to define a flow once and use it in multiple other flows. Long flows can be split into smaller ones, making them easier to understand and maintain.

You can define a call step as:

flows.yml
flows:
parent_flow:
steps:
- call: "child_flow"
child_flow:
steps:
- action: "utter_child_flow"

The call step must reference an existing flow and the child flow can be defined in the same file as the one containing the parent flow or a different file.

Behavior of the call step

The call step is designed to behave as if the child flow is part of the parent flow. Hence, the following properties come out of the box:

  1. If the child flow has a next: END in any step, the control will get passed back to the parent flow, and the next logical step will be executed.
  2. Slots of the child flow can be filled upfront, even before the child flow is started. The collect steps in a child flow behave as if they were directly part of the parent flow.
  3. Slots of the child flow can be corrected once they are filled and the control is still inside the child or the parent flow. So, even after the child flow is complete, the slots can be corrected as long as the parent flow is still active.
  4. Slots of the parent flow will not be reset when the child flow is activated. The child flow can access and modify slots of the parent flow.
  5. Slots of a child flow will not be reset when the control comes back to the parent flow. Instead, they will be reset together with the slots of the parent flow once that ends (unless reset_after_flow_ends is set to False for any of the slots).
  6. If the CancelFlow() command is triggered inside the the child flow, the parent flow is also cancelled.
note

To prevent the child flow from getting triggered directly by a user message, you can add a flow guard to the child flow:

flows.yml
flows:
parent_flow:
steps:
- call: "child_flow"
child_flow:
if: False
steps:
- action: "utter_child_flow"

In the above example, the child_flow will never be triggered directly by a user message but only when the parent flow calls it.

Constraints on the call step

Calling other flows has the following constraints by design:

  • A child flow can not use the link step because a link step is meant to always terminate the previous flow which would contradict with the behaviour of call step where the control should always be passed to the parent flow.
  • Patterns can not use the call step.
Recommendation on using link v/s call

If the use case demands a flow to be initiated as a follow-up to another flow, then a link step is better suited to accomplish the connection between the two flows. However, if a flow needs to behave as if it was part of another larger flow and more steps need to be executed inside the larger flow after the child flow has completed, then call step should be used.

Set Slots

A set_slots step is used to set one or more slots. For example:

- set_slots:
- account_type: "savings"
- account_number: "123456789"

Normally, slots are either set from user input (using a collect step) or by fetching information from another system (using a custom action).

A set_slot is mostly useful to unset a slot value. For example, if a user asks to transfer more money than they have available in their account, you might want to inform them. Just reset the amount slot rather than ending the flow.

flows.yml
flows:
transfer_money:
description: This flow lets users send money to friends and family.
steps:
- collect: recipient
- id: "ask_amount"
collect: amount
description: the number of US dollars to send
- action: action_check_sufficient_funds
next:
- if: not has_sufficient_funds
then:
- action: utter_insufficient_funds
- set_slots:
- amount: null
next: "ask_amount"
- else: final_confirmation

Noop

A noop step can be combined with the next property to create a conditional branch in a flow without necessarily running an action, uttering a response, collecting a slot or linking / calling another flow in that step itself.

For example:

flows:
change_address:
description: Allow a user to change their address.
steps:
- noop: true
next:
- if: not slots.authenticated
then:
- call: "authenticate_user"
next: "ask_new_address"
- else: "ask_new_address"
- id: "ask_new_address"
collect: "address"
...

It's always necessary to add the next property to a noop step. Otherwise, validation of flows would fail during training.

Examples

A basic flow with branching

flows:
basic_flow_with_branching:
description: "a flow with a branch"
steps:
- action: "utter_greet"
- collect: "age"
next:
- if: age < 18
then: "too_young_step"
- else: "old_enough_step"
- id: "too_young_step"
action: "utter_too_young"
- id: "old_enough_step"
action: "utter_old_enough"

The example shows a flow that branches based on the age slot collected from a user message.

Linking multiple flows

flows:
transfer_money:
description: "Send money to another individual"
steps:
- collect: recipient_name
- collect: amount_of_money
- action: execute_transfer
- action: utter_transfer_complete
- link: collect_feedback
collect_feedback:
description: "Collect feedback from user on their experience talking to the assistant"
steps:
- collect: ask_rating
- action: utter_thankyou

This example demonstrates how the link step in flows enables the start of another flow as a follow-up flow. Specifically, the flow collect_feedback is initiated as a follow up flow after transfer_money is terminated at the link step.

Embedding a flow inside another flow

Let's assume a financial assistant needs to serve two use cases:

  1. Transferring money to another individual
  2. Adding a new recipient for transferring money

It's possible that an end user talking to the assistant wants to initiate a money transfer to an existing recipient, hence not needing the second use case. However, it is also possible that the user wants to transfer money to a new recipient in which case both the use cases need to be combined. This is exactly where a call step lets you accomplish both the possibilities without needing to create redundant flows. To accomplish the above use cases, you can leverage the call step in your flows as shown below:

flows.yml
flows:
collect_recipient_details:
description: Details of a money transfer recipient should be collected here
if: False
steps:
- collect: recipient_name
description: Name of a recipient who should be sent money
- collect: recipient_iban
description: IBAN of a recipient who should be sent money.
- collect: recipient_phone_number
description: Phone number of a recipient who should be sent money.
add_recipient:
description: User wants to add a new recipient for transferring money
steps:
- call: collect_recipient_details
- action: add_new_recipient
transfer_money:
description: User wants to transfer money to a new or existing recipient
steps:
- action: show_existing_recipients
- collect: need_new_recipient
next:
- if: slots.need_new_recipient
then:
- call: add_recipient
next: "get_confirmation"
- else:
- call: collect_recipient_details
next: "get_confirmation"
- id: "get_confirmation"
collect: transfer_confirmation
ask_before_filling: true
- action: execute_transfer

For the above flow structure, the LLM's prompt would contain the following flow definition:

prompt.txt
add_recipient: User wants to add a new recipient to transfer money
slot: recipient_name (Name of a recipient who should be sent money)
slot: recipient_iban (IBAN of a recipient who should be sent money)
slot: recipient_phone_number (Phone number of a recipient who should be sent money)
transfer_money: User wants to transfer money to a new or existing recipient
slot: need_new_recipient ((True/False))
slot: recipient_name (Name of a recipient who should be sent money)
slot: recipient_iban (IBAN of a recipient who should be sent money)
slot: recipient_phone_number (Phone number of a recipient who should be sent money)
slot: transfer_confirmation ((True/False))

Each of the above flows are quite self-contained, accomplishing a single purpose and reused effectively to accomplish combinations of multiple use cases in a single conversation. This enhances the modularity and maintainability of flows. Also, note that no slot of a child flow is overlapping with slot of its parent flow in terms of the information they capture. This is important for the command generator's LLM to not get confused at filling such slots.

Optionally ask for information

Imagine a dress shopping AI assistant created to help users find and purchase dresses. This AI assistant includes two types of features: primary features (i.e., dress type, size) and optional features (i.e., color, material, price range, etc.). The assistant should ask for the primary features and avoid asking for the optional features, unless the user specifically requests them. This approach is key to avoid asking too many questions and overwhelming the user.

You can achieve this by setting the ask_before_filling property to false on the collect step and setting an initial_value for the slot in the domain file.

flows:
purchase_dress:
name: purchase dress
description: present options to the user and purchase the dress
steps:
- collect: "dress_type"
- collect: "dress_size"
- collect: "dress_color"
ask_before_filling: false
- collect: "dress_material"
ask_before_filling: false
next:
- if: "slots.dress_material == 'cotton'"
then:
- action: action_present_cotton_dresses
next: END
- if: "slots.dress_material == 'silk'"
then:
- action: action_present_silk_dresses
next: END
- else:
- action: action_present_all_dresses
next: END
domain.yml
slots:
dress_type:
type: categorical
values:
- "shirt"
- "pants"
- "jacket"
mappings:
- type: custom
dress_size:
type: categorical
values:
- "small"
- "medium"
- "large"
mappings:
- type: custom
dress_color:
type: text
initial_value: "unspecified"
mappings:
- type: custom
dress_material:
type: categorical
initial_value: "any"
values:
- "cotton"
- "silk"
- "polyester"
- "any"
mappings:
- type: custom

With ask_before_filling set to false and given that the slots (dress_color and dress_material) already have initial values, the corresponding collect steps will not be asked. Instead, the assistant will move to the next step. If the user explicitly provides a value for a slot, this new value will overwrite the initial one.