> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bubblav.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Flows: Build Multi-Step Chatbot Scenarios Visually

> Compose chatbot scenarios on a visual canvas — match visitor intent, collect answers, run tools, call webhooks, notify your team, and hand off to a human — without writing code.

Sometimes a conversation needs more than a single AI answer: collect a few details, look
something up in your systems, then act on it. **Flows** is a visual builder where you compose
those scenarios as a graph of steps — a trigger that starts the flow, then condition and action
nodes that run in order.

Two examples of what a flow can do:

* **Multi-source answer** — a visitor asks a question → the flow searches your Discourse forum
  and your CRM via Custom Tools → the AI aggregates the results into one answer.
* **Ticket intake** — the flow asks for email, company, and the problem → submits everything to
  your webhook → notifies your team.

<Frame>
  <img className="block dark:hidden" src="https://mintcdn.com/bubblav-e553cf80/nEV72DswoWJ61oM6/images/flows-list.png?fit=max&auto=format&n=nEV72DswoWJ61oM6&q=85&s=ba068b2c862582b21fa51d754875e0ff" alt="Flows list — flow cards with trigger type, run count, and active toggle" width="1834" height="1236" data-path="images/flows-list.png" />

  <img className="hidden dark:block" src="https://mintcdn.com/bubblav-e553cf80/nEV72DswoWJ61oM6/images/flows-list-dark.png?fit=max&auto=format&n=nEV72DswoWJ61oM6&q=85&s=ca2b3a05f84669bc39619bdd6dd3aa8f" alt="Flows list — flow cards with trigger type, run count, and active toggle" width="1834" height="1236" data-path="images/flows-list-dark.png" />
</Frame>

Flows consolidate four older features into one canvas:

* **Smart Triggers** → a `page_behavior` trigger + `send_message` node
* **Human handoff scenarios** → a `visitor_intent` trigger + `request_human` node
* **Forms** → embedded in a flow with a `show_form` node. Lead forms that BubblaV finds on
  your website become ready-made flows automatically — see
  [Flows created from forms on your website](#flows-created-from-forms-on-your-website).
* **Cart Recovery** → a `cart_abandoned` trigger + `delay` + `cart_recovery_touch` nodes

The old Smart Triggers, Forms, and Cart Recovery pages redirect here — everything is built
and edited in the flow editor.

<Note>
  Flows require a **Pro plan**. Messages a flow sends don't consume your monthly message
  quota.
</Note>

## How Flows work

1. A **trigger** starts a run — either a visitor message that matches your intent description,
   or an event like a form submission.
2. The flow walks the graph node by node: asking questions, branching on conditions, calling
   tools and webhooks, sending messages.
3. While a flow is running or waiting for the visitor's answer, it **owns the conversation** —
   the visitor's replies resume the flow instead of going to the AI.
4. The run ends when the graph ends, when an **AI respond** node hands the collected results to
   the chatbot for a final answer, or when the visitor cancels ("cancel", "stop", "never mind").

## Triggers

Every flow has exactly one trigger — the first node on the canvas.

| Trigger | Starts a run when… |
| - | - |
| `visitor_intent` | A visitor's chat message matches your natural-language intent description (e.g. "wants a callback", "asks about order status"). The AI scores each active flow's intent and the best match above the threshold runs. |
| `page_behavior` | The widget detects page behavior — time on page, exit intent, or (Shopify) cart threshold / product page — and the flow opens the conversation proactively. |
| `form_submitted` | A visitor submits one of your BubblaV forms. |
| `conversation_started` | A new conversation is created. |
| `conversation_rated` | A visitor rates a conversation (thumbs up/down). |
| `handoff_requested` | A live-support ticket is created. |
| `lead_captured` | A visitor's email is captured. |
| `cart_abandoned` | (Shopify) A synced abandoned checkout starts a run — the sync runs every 15 minutes. Payload exposes `{{trigger.email}}`, `{{trigger.recoveryUrl}}`, `{{trigger.totalPrice}}`, `{{trigger.currency}}`, `{{trigger.items}}`. |
| `inbound_webhook` | An external system POSTs to the flow's secret webhook URL — for events from your own backend. |

`visitor_intent` and `page_behavior` are the **chat triggers**: the flow runs inside the
conversation and can talk to the visitor. The others are **event triggers** that run in the
background. Event triggers that carry a conversation (`form_submitted`,
`conversation_started`, `handoff_requested`) can still message the visitor;
`conversation_rated`, `lead_captured`, `cart_abandoned`, and `inbound_webhook` have no
conversation, so chat nodes aren't allowed under them — the editor enforces this.

### Page behavior triggers

A `page_behavior` trigger turns the flow into a proactive opener: the widget watches the
visitor's page behavior and, when it matches, the flow starts a conversation with an
AI-personalized message.

| Subtype | Fires when… |
| - | - |
| `time_on_page` | The visitor has spent a set dwell time on a matching page. |
| `exit_intent` | The visitor is about to leave (mouse toward the top on desktop, tab switch on mobile). |
| `cart_threshold` | (Shopify) The cart total crosses your configured amount. |
| `product_page` | (Shopify) The visitor dwells on a product page. |
| `new_visitor` | The page loads during the visitor's first session on your site. |
| `returning_visitor` | The page loads for a visitor returning after their first session (\~30 min window). |

Config: `subtype`, `urlMatchType` (`exact`/`contains`/`prefix`/`regex`), `urlPatterns`,
`dwellSeconds`, `cooldownMinutes`, `sortOrder`, plus `config.cart_threshold` for e-commerce
subtypes (`threshold_cents`, `min_cart_cents`, `currency`). `new_visitor` and `returning_visitor`
fire on page load and ignore `dwellSeconds`. When the static greeting message is
enabled it owns the first pageview — a `new_visitor` flow then fires on the next
page view or SPA navigation once the greeting is dismissed or no longer showing.
Pair it with a `send_message` node for the opener — or `generate_text` → `send_message`
when you want the AI to write the opener from instructions.

## Node catalog

Nodes are the steps of your flow. Drag them from the palette onto the canvas and connect them.

<Frame>
  <img className="block dark:hidden" src="https://mintcdn.com/bubblav-e553cf80/nEV72DswoWJ61oM6/images/flows-node-config.png?fit=max&auto=format&n=nEV72DswoWJ61oM6&q=85&s=ffc4fd5b3f62a26106ae20a346066007" alt="Node configuration panel — a Collect input node's question and expected value" width="1834" height="1584" data-path="images/flows-node-config.png" />

  <img className="hidden dark:block" src="https://mintcdn.com/bubblav-e553cf80/nEV72DswoWJ61oM6/images/flows-node-config-dark.png?fit=max&auto=format&n=nEV72DswoWJ61oM6&q=85&s=5c076974489daddf107e9b425c068c81" alt="Node configuration panel — a Collect input node's question and expected value" width="1834" height="1584" data-path="images/flows-node-config-dark.png" />
</Frame>

| Node | What it does | Settings (`data`) |
| - | - | - |
| `trigger` | Entry point — exactly one per flow. | `triggerType`, plus `intent` (required for `visitor_intent`) |
| `condition` | Branches the flow. Its outgoing edges are labeled `true`/`false`. | `mode: 'context'` → `field`, `op` (`equals`, `not_equals`, `contains`, `exists`, `not_exists`), `value`; or `mode: 'ai'` → `question` answered against collected variables |
| `delay` | Parks the run until a timer fires, then resumes on the next node. Visitor messages during the wait go to the AI, not the flow. | `minutes` |
| `collect_input` | Sends a question as a bot message and waits; the visitor's reply is stored in a variable. | `question`, `variable`, `expected` (optional, e.g. `email`, `phone` — the AI checks the reply contains a usable value and re-asks once if not), `options` (optional quick-reply chips — max 6, or max 5 when `optional` is on), `optional` (adds a **Skip** chip as the 6th chip; clicking it or replying "skip", "pass", or "n/a" stores an empty answer that notifications leave out — a reply like "no" is kept as the answer) |
| `show_form` | Sends a bot message with one of your forms embedded and waits for the submission. | `formId`, `introMessage`, `variable` |
| `run_tool` | Runs one of your Custom Tools or MCP tools and stores the result. | `toolId`, `args` (values can be literals, `{{var}}`, or JSON objects/arrays matching the tool's schema), `variable` |
| `send_message` | Sends a bot message. | `message` |
| `call_webhook` | Calls an external URL with the same SSRF and timeout guards as Custom Tools; optionally signs the request (HMAC) and stores the response. | `url`, `method` (`POST`/`GET`/`PUT`/`PATCH`), `headers`, `body`, `secret`, `variable` |
| `send_email` | Sends a transactional email from `BubblaV <noreply@bubblav.com>` — no domain setup needed. Set `replyTo` (e.g. `{{contact.email}}` from a **Show form** node, i.e. `{{<variable>.<fieldId>}}`) so your team can reply straight to the visitor. An empty `replyTo` is omitted. An invalid one is skipped with a warning in the step output, and the email is still sent. | `to`, `subject`, `body`, `replyTo` (optional) |
| `notify_team` | Sends a notification to your website's team members (bell, push, and email per their preferences). The notification lists the visitor's answers to the flow's **Collect input** questions (form submissions from **Show form** are not included) and links to the run in **Flow Runs**. | `title`, `message` (both default to a generic text when empty), `includeData` (default on — attach the collected answers), `recipientIds` (optional — notify only the chosen team members; default is the whole team, and the whole team is notified if none of the chosen members is still on it) |
| `request_human` | Sends `message` to the visitor, then creates a live-support ticket so a teammate can take over. The ticket uses the email from a **Collect input** question with the variable `email` (BubblaV finds the address in the answer, e.g. "Acme Ltd, [jo@acme.com](mailto:jo@acme.com)"). If there is no such answer, it uses the email already on file for the visitor. Your team gets an AI summary of the conversation, not `message`. | `message` (shown to the visitor) |
| `ai_respond` | **AI answer — terminal, chat flows only.** The chatbot answers the visitor using the knowledge base and all collected variables. Must be the last node. | `instructions` (optional) |
| `generate_text` | Generates text mid-flow into a variable — e.g. summarize the conversation for a `send_email` body. Includes the transcript automatically. | `instructions` (required), `variable` (required) |
| `cart_recovery_touch` | Sends an AI-written recovery message for the run's abandoned cart — chat widget if the visitor is reachable (first touch only), else an unsubscribe-compliant email with a one-click recovery link. Skips when the cart is recovered, expired, or suppressed. | `aiInstructions` (optional), `discountCode` (optional) |

## Variables and `{{var}}` interpolation

Every value a flow collects — visitor replies, form submissions, tool results, webhook
responses — is stored in a named **variable**. Use `{{variable}}` (or a nested path like
`{{order.status}}`) inside any message, question, webhook body, or email to insert the value.
The trigger's payload is available as `{{trigger}}`. Missing variables resolve to an empty
string.

## Build a flow

<Steps>
  <Step title="Open Flows">
    In the dashboard, open your website and go to **Flows**, then click
    **New Flow**.
  </Step>

  <Step title="Name it and pick a trigger">
    Give the flow a name and choose what starts it — a visitor intent or an event.
  </Step>

  <Step title="Compose the canvas">
    Drag nodes from the palette and connect them. Click a node to configure it in the side
    panel — pick forms and tools from your website's existing ones, or create a new one from
    the panel without leaving the editor.
  </Step>

  <Step title="Test it">
    The editor autosaves as you work (watch for "Saving…"/"Saved" in the header). Open the
    **Test** panel to run the flow — chat with it like a visitor, or fire a test event for
    event triggers.
  </Step>

  <Step title="Activate">
    When the test run looks right, switch the flow to **active**. New flows are saved inactive
    — they never run until you turn them on.
  </Step>
</Steps>

<Frame>
  <img className="block dark:hidden" src="https://mintcdn.com/bubblav-e553cf80/nEV72DswoWJ61oM6/images/flows-editor.png?fit=max&auto=format&n=nEV72DswoWJ61oM6&q=85&s=e91f0790b33a9e03031a552bae62b570" alt="Flow editor — node palette, canvas with connected nodes, and test button" width="1834" height="1584" data-path="images/flows-editor.png" />

  <img className="hidden dark:block" src="https://mintcdn.com/bubblav-e553cf80/nEV72DswoWJ61oM6/images/flows-editor-dark.png?fit=max&auto=format&n=nEV72DswoWJ61oM6&q=85&s=26a6b198c769a03e6f3f6b64ce205f42" alt="Flow editor — node palette, canvas with connected nodes, and test button" width="1834" height="1584" data-path="images/flows-editor-dark.png" />
</Frame>

<Tip>
  Stuck? Click **Ask BubblaV** in the editor toolbar and describe the scenario — the assistant
  can draft the flow on your canvas for you.
</Tip>

## Flows created from forms on your website

You don't always start from an empty canvas. When BubblaV crawls your website — when you add
the website, and again on each re-crawl — it looks for lead forms on your pages: contact,
quote request, price request, service booking, and appointment booking forms. For each lead
form, BubblaV creates a chat flow that collects the same information in chat.

Each flow has this shape:

`visitor_intent` → one `collect_input` per form question → `notify_team` → `send_message`

* **Trigger** — a `visitor_intent` that names one goal, for example "Use when a visitor wants
  to request a quote for a custom order."
* **Questions** — one short chat question per important field. First and last name become one
  "full name" question, and address parts become one address question. Required fields come
  first. Optional fields get a **Skip** chip. Email, phone, date, and number fields set
  `expected`, so the flow asks again once if the answer has the wrong shape. Select and radio
  fields with 6 or fewer choices become quick-reply chips.
* **Notify team** — sends your team a bell, push, and email notification with the visitor's
  answers.
* **Confirmation** — a short message that tells the visitor your team will follow up.

Search, filter, login, signup, newsletter, cart, checkout, and review forms are ignored.

<Steps>
  <Step title="Find the flows">
    Open **Flows**. Detected flows have names that start with **Form:** and the description
    "Created from a form detected on your site." When the first crawl finishes, the
    website-ready email also lists them, with the page where each form was found.
  </Step>

  <Step title="Review the questions">
    Open the flow and check each question, the intent, and the confirmation. Edit them like
    any other flow — for example, add a `call_webhook` node to send the answers to your CRM.
  </Step>

  <Step title="Test and activate">
    Run the flow in the **Test** panel, then switch it to **active**. Detected flows are
    created **inactive** and never run until you turn them on.
  </Step>
</Steps>

<Note>
  A website has at most **10 detected forms** that wait for review or became flows. A flow
  that you delete is not created again on the next crawl. Activating a flow requires a **Pro
  plan**.
</Note>

## Test before activating

The editor's **Test** panel runs the flow without touching real conversations. Clicking
**Test** saves any pending canvas edits first, so the run always exercises what you see.

* For `visitor_intent` flows, a chat-style transcript lets you play the visitor — send messages,
  answer the flow's questions, and watch each step's status. The panel reports the intent-match
  score your message would get in production, and typing "cancel" or "stop" exercises the same
  escape hatch visitors have. `show_form` nodes render the real form inline — submitting it
  resumes the run, and an `ai_respond` node previews the actual AI reply.
* For event triggers, fire a test event with a JSON payload preview. If the run then waits on a
  chat node (`collect_input`, `show_form`), the input row switches to a chat box so you can
  continue the run like a visitor.
* While a test runs, the canvas highlights the executed path — green for completed nodes, red
  for failures, and a pulsing amber ring on the node the run is waiting on.
* Each step in the trace expands to show its exact input and output JSON, so you can see what
  data flowed between nodes. Steps that touch the outside world (`run_tool`, `call_webhook`,
  `send_email`, `notify_team`, `request_human`, `cart_recovery_touch`) carry a `live` badge.

<Warning>
  Test runs execute **real side effects** — tools run, webhooks are called, emails are sent.
  Only message delivery is simulated (nothing is sent to a visitor). Point test webhooks at a
  service like webhook.site if you don't want to hit production endpoints.
</Warning>

<Frame>
  <img className="block dark:hidden" src="https://mintcdn.com/bubblav-e553cf80/nEV72DswoWJ61oM6/images/flows-test.png?fit=max&auto=format&n=nEV72DswoWJ61oM6&q=85&s=4955c077e55d0bfc8a84b1f31171de53" alt="Test panel — visitor transcript and per-step status" width="1834" height="1584" data-path="images/flows-test.png" />

  <img className="hidden dark:block" src="https://mintcdn.com/bubblav-e553cf80/nEV72DswoWJ61oM6/images/flows-test-dark.png?fit=max&auto=format&n=nEV72DswoWJ61oM6&q=85&s=994e9499ff0a040a589d069f714b02cd" alt="Test panel — visitor transcript and per-step status" width="1834" height="1584" data-path="images/flows-test-dark.png" />
</Frame>

## Runs and history

Every execution is recorded as a **run**. Open a flow's **Runs** page to see each run's status
(`running`, `waiting`, `completed`, `failed`, `cancelled`), when it started, and a per-step
trace showing every node's input, output, or error — the fastest way to debug a flow that
didn't behave as expected.

<Frame>
  <img className="block dark:hidden" src="https://mintcdn.com/bubblav-e553cf80/nEV72DswoWJ61oM6/images/flows-runs.png?fit=max&auto=format&n=nEV72DswoWJ61oM6&q=85&s=59c5480eebdd3f24365985bb399eb050" alt="Flow runs — a completed run with its per-step trace expanded" width="1834" height="796" data-path="images/flows-runs.png" />

  <img className="hidden dark:block" src="https://mintcdn.com/bubblav-e553cf80/nEV72DswoWJ61oM6/images/flows-runs-dark.png?fit=max&auto=format&n=nEV72DswoWJ61oM6&q=85&s=05c9e10d3c8d5deee9523f7fdd2e32e4" alt="Flow runs — a completed run with its per-step trace expanded" width="1834" height="796" data-path="images/flows-runs-dark.png" />
</Frame>

A run that waits more than 24 hours for a visitor reply is cancelled automatically — timer
waits on a `delay` node are exempt and resume when the timer fires. A visitor can always
escape a waiting flow by saying "cancel" or "stop" — the conversation then goes back to the
normal AI.

## Debug and troubleshoot

When a flow misbehaves, work through it in this order:

1. **Reproduce in the Test panel.** The transcript, intent-match score, and canvas highlight
   show exactly which path executed. Expand each step's input/output to see the data it
   received and produced.
2. **Check the Runs page.** Every real and test execution is recorded. A `failed` step shows
   its error inline; a `waiting` run means the flow is parked on a `collect_input` or
   `show_form` node expecting a visitor reply.
3. **Common causes:**
   * *Flow never triggers* — the visitor's phrasing doesn't match the intent closely enough.
     Test the exact message in the panel; if the score is below the 0.6 threshold, broaden the
     intent description.
   * *Flow stops mid-run* — a `waiting` status means it's waiting for input, not broken. Check
     whether the visitor escaped with "cancel"/"stop" (status `cancelled`) or the run timed
     out after 24h.
   * *Webhook/tool step failed* — expand the step output for the HTTP status or error. Test
     webhooks against a service like webhook.site first.
   * *AI reply looks wrong* — `ai_respond` composes from the collected variables; check the
     step inputs to confirm the right values were captured, and tighten the node's
     `instructions`.

## Example flows

Six recipes to copy — each is a chain of nodes you can build in a few minutes.

### Lead qualification

`visitor_intent` ("interested in pricing / wants a quote") → `collect_input` email
(`expected: email`) → `collect_input` company → `call_webhook` to your CRM → `notify_team` →
`send_message` confirmation.

The visitor asks about pricing, the flow collects their email and company, posts the lead to
your CRM, pings your team, and confirms to the visitor — all inside one conversation.

### Proactive opener (Smart Trigger)

`page_behavior` (`time_on_page`, 30s on `/pricing*`) → `generate_text` → `send_message` → `ai_respond`.

The widget watches visitors dwell on your pricing page, opens the chat with an
AI-written opener, then lets the chatbot answer follow-ups. This is the old Smart
Triggers pattern as a flow.

### Human handoff

`visitor_intent` ("wants to speak to a human / asks for sales") → `collect_input` email →
`request_human`.

The old handoff-scenario pattern: the flow matches the visitor's intent, optionally collects
contact details first, then creates a live-support ticket so a teammate can take over with
the full transcript attached. See [Human handoff](/user-guide/human-handoff).

### Form in a flow

`visitor_intent` ("wants a demo / wants to sign up") → `show_form` (your signup form) →
`send_message` confirmation.

Instead of the AI deciding when to surface a form, the flow controls it: the form renders
in chat at exactly the step you place it, and the submission lands in a variable you can
pass to a webhook or email downstream.

### Ticket intake (event)

`inbound_webhook` → `condition` on `{{trigger.priority}}` → `send_email` to support /
`notify_team`.

Your backend POSTs a ticket payload to the flow's webhook URL; the flow routes high-priority
tickets to the support inbox and notifies the team about the rest. Event triggers have no
visitor conversation, so only non-chat nodes are used.

### Cart recovery (Shopify)

`cart_abandoned` → `delay` 60m → `cart_recovery_touch` → `delay` 24h → `cart_recovery_touch`.

Every synced abandoned checkout starts a run: the flow waits an hour, sends an AI-written
recovery message (chat widget if the visitor is still around, email otherwise), waits a day,
then sends one follow-up. A completed purchase cancels the run mid-wait. See
[Shopify](/user-guide/integrations/shopify#ai-abandoned-cart-recovery).

## Best practices

* **Write intents like a visitor would phrase them** — "wants to speak to sales", not "sales
  intent trigger".
* **Collect one thing per `collect_input`** — short questions get better answers, and each
  answer lands in its own variable.
* **Set `expected` when the answer has a shape** — `email`, `phone`, `order number` — so the
  flow re-asks once instead of storing "I don't know".
* **End chat flows with `ai_respond` or `send_message`** — don't leave the visitor hanging on a
  silent last step.
* **Test with real-looking data** — `run_tool` and `call_webhook` hit your real endpoints even
  in test runs.

## Plans & limits

Flows require a **Pro plan** or higher. There's no limit on the number of flows per website.
Messages a flow sends don't consume your plan's message quota. See
[Billing & Plans](/user-guide/billing) for details.

## Related

<CardGroup cols={2}>
  <Card title="Custom Tools" icon="wrench" href="/user-guide/integrations/custom-tools">
    The webhook tools a flow can call with `run_tool`.
  </Card>

  <Card title="Forms" icon="file-text" href="/user-guide/forms">
    Build the forms a `show_form` node embeds in chat.
  </Card>

  <Card title="Human handoff" icon="headset" href="/user-guide/human-handoff">
    What happens after a `request_human` node creates a ticket.
  </Card>

  <Card title="Widget design" icon="palette" href="/user-guide/widget-design">
    How the chat widget looks when a flow opens with your message.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.