| name | rasa-building-flows |
| description | Builds conversation flows for Rasa CALM assistants using YAML. Use when creating or editing flows, designing flow architecture, adding branching logic, writing flow and collect step descriptions, or connecting flows with call and link steps.
|
| license | Apache-2.0 |
| metadata | {"author":"rasa","version":"0.1.0","rasa_version":">=3.9.0","docs-url":"https://rasa.com/docs/reference/primitives/flows/"} |
Building Flows in Rasa
Workflow
- Identify the business process the user wants to implement.
- Review existing flows, slots, responses, and actions. Reuse before creating new ones.
- Scope the work — one file per business process, one flow per user goal
(see "Scoping a flow").
- Create or open the YAML file under
data/, named after the primary flow
(see "File organization").
- Define the flow with a
snake_case ID (see "Naming conventions").
- Write the flow
description (see "Writing descriptions").
- Add steps:
collect for user input, action for responses or invoking custom
actions.
- Write descriptions for
collect steps (see "Writing descriptions").
- Add branching with
next/if/then/else where the process has decision points
(see "Branching and conditions").
- Extract reusable sub-tasks into child flows via
call (see "Connecting flows").
- Connect follow-up flows via
link if a separate process should start after.
- Ensure all referenced slots, responses, and actions exist in the domain
(see "Ensure domain completeness").
- Validate the project (see "Fixing validation errors").
Scoping a flow
A flow represents one business process from the user's perspective. Use these
questions to decide whether something warrants its own flow:
- Does a user explicitly ask for this? → yes = flow
- Can it run independently from start to finish? → yes = flow
- Is it a sub-task reused by multiple flows? → yes = child flow with
if: False
- Is it just one action at the end of another flow? → no, keep it inline
Right-sized flow - a single user goal with a clear start and end:
flows:
track_order:
description: Lets users check the delivery status of an existing order.
steps:
- collect: order_id
- action: action_fetch_order_status
- action: utter_order_status
Too big - multiple unrelated goals in one flow. Split them:
flows:
shopping:
description: Handles all shopping operations.
steps:
- collect: operation_type
Too small - a single step that is not a business process. Inline it:
flows:
say_goodbye:
description: Says goodbye.
steps:
- action: utter_goodbye
File organization
Before creating new files, check the existing project layout. If the project already
uses a different convention, follow that convention instead.
By default, all flow files live under data/. Subdirectories are supported — Rasa
recursively scans data/ for YAML files. Each file groups a business process with its
sub-flows:
data/
├── book_appointment.yml # flow + child flows in one file
├── track_order.yml
├── transfer_money.yml
└── onboarding/ # subdirectory for a larger domain area
├── register_patient.yml
└── verify_insurance.yml
Name the file after the primary flow it contains. If book_trip has child flows
like select_destination and choose_flights, all three go in data/book_trip.yml.
Use subdirectories to group related flows by domain area when the project grows large
(e.g. data/payments/, data/scheduling/). The directory structure has no functional
impact — it's purely organizational.
Naming conventions
This section proposes default conventions. If the user's project already
follows different conventions, match those instead.
Flow IDs
- Use
snake_case: transfer_money, book_appointment, track_order
- Start with a verb:
check_balance, schedule_visit — not flow_1 or appointment
- Child flows can use a prefix of the parent for clarity:
book_appointment_collect_patient_info
Flow names
The optional name field is a human-readable label. It has no functional impact.
flows:
book_appointment:
name: "Book Appointment"
description: Helps patients schedule a visit with their doctor.
Writing descriptions
Flow descriptions
The description is the primary signal the LLM within the Command Generator uses to
decide when to start a flow. A bad description means the flow won't trigger — or
triggers for the wrong request.
- Be information-dense: precisely outline the flow's purpose and scope. Avoid filler
words. Use imperative language.
- Use clear, standard language: avoid unusual phrasing. Stick to universally
understood terms.
- Explicitly define context: the embedding model lacks situational awareness - it
can't read between the lines. State the domain and intent directly.
- Clarify specialized knowledge: if the flow references brand names or product
names, explain what they are. The embedding model won't know.
- Keep descriptions distinct: if two flows sound similar, add distinguishing details
so the LLM within the Command Generator can disambiguate.
book_appointment:
description: Helps patients schedule a visit with their doctor.
track_order:
description: Lets users check the delivery status of an existing order.
book_appointment:
description: Runs action_book_slot and fills the date slot.
track_order:
description: A flow for orders.
Collect step descriptions
The description on a collect step tells the LLM within the Command Generator
what value to extract from the user's message. Without it, the LLM within
the Command Generator only has the slot name to guess.
- Describe the expected value, not the question being asked
- Include format, constraints, or valid values when applicable
- Provide examples for ambiguous or structured data
- collect: insurance_id
description: >
Insurance member ID printed on the patient's card.
Alphanumeric, 9-12 characters (e.g. XYZ-123456789).
- collect: return_reason
description: "Reason for the return: damaged, wrong_item, changed_mind, or other."
- collect: amount
description: the amount
- collect: email
description: Ask the user for their email address.
Slots and collect steps
By default, slots filled in collect or set_slots steps reset when the flow ends (to
null or to the slot's initial_value from the domain). To retain a slot's value after
the flow completes, list it in persisted_slots at the flow level. Slots set in custom
actions are persistent by default and should not be listed in persisted_slots.
flows:
return_item:
description: Helps users return a purchased item for a refund or exchange.
persisted_slots:
- order_id
- return_reason
steps:
- collect: order_id
- collect: return_reason
- action: action_process_return
A collect step automatically uses a response named utter_ask_{slot_name} to prompt
the user. Override this with the utter property on the collect step, or use a custom
action named action_ask_{slot_name} instead — but not both.
- collect: account_type
utter: utter_ask_secondary_account_type
Use rejections on a collect step to validate values inline. The assistant re-asks
automatically on rejection. For complex validation, create a custom action named
validate_{slot_name}.
- collect: age
rejections:
- if: slots.age < 1
utter: utter_invalid_age
- if: slots.age < 18
utter: utter_must_be_18
To reset a slot mid-flow, set it to null in a set_slots step. In a custom action,
return None. Empty slots are not eligible for user correction.
- set_slots:
- amount: null
See the full reference for all collect step properties (ask_before_filling,
force_slot_filling, utter, etc.).
Branching and conditions
Any step can branch by adding a next property with if/then/else conditions.
Conditions support operators like and, or, not, <, >, =, !=, is,
is not, contains, and matches (see full reference for details).
Two namespaces are available in conditions:
slots. — slot values: slots.age < 18, slots.plan = "premium"
context. — dialogue frame properties: context.previous_flow_name,
context.collect, context.canceled_name
When context.collect or context.validate reference a slot name, wrap them in Jinja
with the slots. prefix: "slots.{{context.collect}} is not null"
When a condition leads to next: END, the flow completes without running further steps.
Use a noop step when you need a branch point without performing any action.
- collect: membership_tier
next:
- if: slots.membership_tier = "premium"
then: premium_step
- if: slots.membership_tier = "standard"
then: standard_step
- else:
- action: utter_unknown_tier
next: END
- id: premium_step
action: utter_premium_benefits
- id: standard_step
noop: true
next:
- if: not slots.verified_email
then:
- call: verify_email
next: continue_step
- else: continue_step
- id: continue_step
action: utter_standard_benefits
Connecting flows: call vs link
Two step types connect flows together. Choose based on whether the current flow should
continue or end:
| call | link |
|---|
| When | Sub-task needed, then parent continues | Current process is done, different one starts |
| Control | Returns to parent after child completes | Current flow ends, target starts fresh |
| Position | Anywhere in the flow | Must be the last step |
| Slots | Child slots accessible in parent | No slot sharing — target flow is independent |
| Cancellation | Cancelling child cancels parent too | N/A — current flow already ended |
Example — an orchestrator flow that sequences call steps for sub-tasks and ends
with link to hand off to a separate process. Child flows should not overlap in the
slots they collect, so the LLM within the Command Generator can unambiguously
route user input.
flows:
book_trip:
description: Guides the user through booking a complete trip with flight and hotel.
steps:
- call: book_flight
- call: book_hotel
- action: utter_trip_summary
- link: process_payment
book_flight:
description: Helps the user search and select a flight.
if: False
steps:
book_hotel:
description: Helps the user search and select a hotel at the destination.
if: False
steps:
process_payment:
description: Handles payment for a completed booking using a saved or new card.
steps:
Flow guards
Guards control whether a flow can be started by the LLM within the Command Generator:
view_medical_records:
if: slots.authenticated AND slots.identity_verified
collect_patient_info:
if: False
Ensure domain completeness
Every element a flow references must be defined in the domain (domain.yml or split
domain files). After writing or editing a flow, walk through each step and verify the
items below are present. Missing entries cause validation errors or silent runtime
failures.
- Every slot used in the flow →
slots:
(see rasa-managing-slots)
- Every
collect: slot_name → utter_ask_{slot_name} in responses:
(see rasa-writing-responses)
- Every
action: utter_* step → matching entry in responses:
- Every custom action / validation action →
actions:
(see rasa-writing-custom-actions)
- Rejection responses →
responses:
Fixing validation errors
When validation fails, preserve the user's intent — what the flow is supposed to
accomplish. The flow steps themselves can change, but the intended behavior must not be
lost.
Never strip down a flow's intended behavior to make validation pass. If needed, fix the
domain: slots, responses, and actions to support what the user asked for.