Skip to content

Conversational Flows

Build structured conversation paths for lead capture, booking, and multi-step processes

View .md

Conversational Flows

Flows are deterministic conversation paths that guide users through structured interactions. While the AI agent handles free-form questions, flows ensure consistent data collection and multi-step processes.

What are Flows?

A Flow is a series of steps that execute in sequence when triggered. Each step can:

  • Send a message
  • Collect and validate user input
  • Branch based on conditions
  • Call webhook tools
  • Generate AI responses
  • Transfer to a human agent
Code
[Trigger] → [Welcome] → [Collect Name] → [Collect Email] → [Confirm] → [End]

When to Use Flows

ScenarioUse FlowUse Free-form LLM
Collecting required data (name, email, phone)YesNo
Compliance-required messagingYesNo
Appointment bookingYesMaybe
General Q&ANoYes
Product recommendationsNoYes
Order status lookupMaybeYes

Flow Triggers

Three trigger types run today:

Trigger TypeDescriptionExample
keywordMessage contains any of the words, case-insensitive substring match"book", "appointment", "schedule"
intentMessage expresses the intent you described, judged by a classifier rather than by matching words"wants a price or a quote"
alwaysEvery inbound message that is not already inside a flowWelcome/onboarding flow

always restarts once the flow completes, so the contact's next message begins it again. It is not once per conversation.

When a flow says nothing

A flow that ends on the message that started it, without replying, hands that message to the agent, and the agent answers it. A flow that only decides it has nothing to do does not leave the contact unanswered.

Three things are silence on purpose, and stay silent:

  • A transfer step, even with an empty message. This is how you keep the agent quiet while a person handles a contact.
  • An llm step that chose not to reply.
  • A flow that already spoke. Once a flow has sent the contact something, the exchange is the flow's: if it later ends on a quiet step, the agent does not step in on that message.

Intent triggers

Write trigger.intent as one plain sentence describing what the contact wants: "wants a price or a quote", "asks where their order is". Any language works, and the contact does not have to write in the same one. A single word such as "sales" is accepted, but a sentence leaves less to guess.

JSON
{ "type": "intent", "intent": "asks where their order is" }

It is read for meaning, not for words. "I don't want pricing info, stop writing to me" contains the word and does not start a pricing flow. A keyword trigger on "pricing" would.

What decides which flow starts:

  • Priority still comes first. A keyword or always flow with a higher priority wins without the intent being considered. An always flow above your intent flows means they never start; give intent flows the higher number.
  • An intent flow starts only on a clear match. When no intent matches, the next keyword or always flow in priority order starts. If there is none, the agent answers.
  • At most 12 intent flows are considered per message, the 12 with the highest priority. An intent longer than 300 characters is cut at 300.
  • If the classifier is unavailable, the message is handled as if no intent matched. The contact still gets an answer; the intent flow does not start.
  • Reading the intent adds time before the reply, typically under a second, and only on messages that no higher-priority trigger claimed. Agents without intent flows are unaffected.

A bare "STOP" or an order number carries no intent to read. Use a keyword trigger for exact words.

Step Types

Every step has the same envelope. config is required, and its fields depend on type:

JSON
{ "id": "<your-step-id>", "type": "message | collect | condition | tool | llm | transfer", "config": { }, "nextStepId": "<id of the next step, or omit>" }

That is the envelope, not a runnable step: config is empty because what goes in it depends on type. Each section below gives the real shape.

Steps run in array order unless a step names nextStepId, or a condition sends the run elsewhere. A step with no nextStepId that is last in the array completes the flow.

Message Step

Send a fixed message. {{variables}} are replaced with what earlier steps collected.

JSON
{ "id": "welcome", "type": "message", "config": { "text": "Welcome to Acme! How can I help you today?" }, "nextStepId": "ask_email" }

Collect Step

Ask for something and wait for the customer's next message.

JSON
{ "id": "ask_email", "type": "collect", "config": { "variable": "customer_email", "prompt": "What's your email address?", "validation": { "type": "email" }, "errorMessage": "That does not look like an email address. Try again." }, "nextStepId": "ask_size" }

errorMessage sits beside validation, not inside it.

Validation types

typeExtra fieldsAccepts
textminLength, maxLengthany text
emaila valid address
phonea valid phone number
numbermin, maxnumeric input
dateaccepts anything, see below
choiceoptions (required)one of options

date currently accepts whatever the contact typed, including tomorrow and asdf, and minDate / maxDate are not read. Validate a date in your own handler, or constrain it with choice.

choice is the reason to reach for a flow at all: the answer is constrained to your list, so what reaches your systems is one of a known set rather than whatever the customer typed.

JSON
{ "id": "ask_size", "type": "collect", "config": { "variable": "size", "prompt": "How many people are on your team? Reply 1-10, 11-50, 51-200, or 200+.", "validation": { "type": "choice", "options": ["1-10", "11-50", "51-200", "200+"] }, "errorMessage": "Please pick one of: 1-10, 11-50, 51-200, 200+." }, "nextStepId": "route" }

Condition Step

Branch. if is interpolated first, then compared, so "{{budget}} > 10000" becomes "25000 > 10000".

JSON
{ "id": "route", "type": "condition", "config": { "conditions": [ { "if": "{{size}} == 200+", "goto": "enterprise_path" } ], "defaultGoto": "standard_path" } }

Supported operators are ==, !=, > and <. There is no AND, no OR and no parentheses. For anything more, chain several condition steps or move the decision into a tool.

defaultGoto runs when no condition matches. Without it, and with nothing matching, the flow completes. If it has said nothing so far, the agent answers the message instead (when a flow says nothing).

A condition cannot be the first step unless it has a defaultGoto: creating or updating a flow that way returns 400 invalid_request. Nothing has been collected at the first step, so its conditions come out the same on every message.

{{ }} reads only variables collected earlier in the same flow, by a collect step or a storeResultAs. Contact fields and metadata are not available: {{contact.plan}} is not replaced, and a condition written on it never matches. A name is letters, digits and underscores, with no dots, so {{lead.status}} does not reach inside a stored object either. To branch on something you hold about the contact, look it up in a tool step whose result is a plain value (a string, a number or a boolean) and compare that.

Tool Step

Call one of the agent's tools.

JSON
{ "id": "save_lead", "type": "tool", "config": { "toolName": "create_lead", "params": { "email": "{{customer_email}}", "size": "{{size}}", "source": "whatsapp" }, "storeResultAs": "lead" }, "nextStepId": "confirm" }

params values wrapped in {{ }} are replaced with collected data; anything else is passed through literally. (action appears in older examples and is accepted, but nothing reads it, so it is left out here.) params may be omitted for a tool that takes none. The tool must already exist on the agent before the flow runs.

LLM Step

Let the model answer, inside the flow.

JSON
{ "id": "suggest", "type": "llm", "config": { "systemPrompt": "Suggest three products for a team of {{size}}. Be brief.", "collectVariable": "recommendations" }, "nextStepId": "confirm" }

Transfer Step

End the flow and hand off to a human.

JSON
{ "id": "escalate", "type": "transfer", "config": { "message": "I'm connecting you with a specialist. Please hold." } }

This sends message, marks the session transferred, and silences the agent for that contact. Their next messages are stored and appear in your inbox, and the agent does not answer them: a contact who asked for a person is not talked over by the bot while they wait.

The agent takes the thread back as soon as someone answers. Any outbound message you send to that contact, from the inbox or the API, ends the handoff. There is no button to press and no state to clean up.

Variables

{{name}} in any string is replaced with what an earlier step collected. A variable that was never collected renders as the literal {{name}} in the message the customer receives, so check that every one is populated before it is used.

Variables come from three places:

Step typeField that creates the variable
collectconfig.variable
toolconfig.storeResultAs
llmconfig.collectVariable

Via Dashboard

  1. Navigate to Flows

    Go to Senders > select your sender > Agent tab > Flows section.

  2. Create New Flow

    Click Create Flow and enter:

    • Name: A descriptive name (e.g., "Lead Capture Flow")
    • Description: What this flow does
    • Trigger: How the flow is activated
  3. Add Steps

    Use the visual flow builder to add steps:

    1. Click Add Step
    2. Select the step type
    3. Configure the step parameters
    4. Connect steps by dragging between nodes
  4. Configure Triggers

    Set up how the flow is triggered:

    • For keyword triggers, enter the words that activate the flow
    • For intent triggers, describe in one sentence what the contact wants
    • For always, the flow runs on every inbound message that is not already inside a flow
  5. Test the Flow

    Use the Test button to simulate a conversation and verify each step works correctly.

  6. Activate Flow

    Toggle the flow to Active to enable it for incoming messages.

Via API

Create Flow

import Zavudev from "@zavudev/sdk"; const zavu = new Zavudev({ apiKey: process.env["ZAVUDEV_API_KEY"] }); const flow = await zavu.senders.agent.flows.create("sender_abc123", { name: "Lead Capture", description: "Collect lead information from new contacts", trigger: { type: "keyword", keywords: ["interested", "info", "learn more"], }, steps: [ { id: "welcome", type: "message", config: { text: "Great! I'd love to help. Let me collect some information." }, nextStepId: "get_name", }, { id: "get_name", type: "collect", config: { variable: "customer_name", prompt: "What's your name?" }, nextStepId: "get_email", }, { id: "get_email", type: "collect", config: { variable: "customer_email", prompt: "What's your email address?", validation: { type: "email" }, errorMessage: "That does not look like an email address. Try again.", }, nextStepId: "confirm", }, { id: "confirm", type: "message", config: { text: "Thanks {{customer_name}}! Our team will contact you at {{customer_email}} within 24 hours.", }, }, ], }); console.log("Flow created:", flow.id);

List Flows

const flows = await zavu.senders.agent.flows.list("sender_abc123"); for (const flow of flows.items) { console.log(`${flow.name} (${flow.id}) - ${flow.enabled ? "Active" : "Inactive"}`); }

Update Flow

const flow = await zavu.senders.agent.flows.update( "flow_xyz789", { senderId: "sender_abc123", name: "Updated Lead Capture", enabled: true, } );

Example Flows

Lead Capture Flow

Qualify an inbound lead into three known fields before anyone reads the thread.

JSON
{ "name": "Lead Capture", "trigger": { "type": "keyword", "keywords": ["pricing", "demo", "info"] }, "enabled": true, "priority": 10, "steps": [ { "id": "intro", "type": "message", "config": { "text": "Thanks for your interest! Let me get some details." }, "nextStepId": "get_name" }, { "id": "get_name", "type": "collect", "config": { "variable": "name", "prompt": "What's your name?" }, "nextStepId": "get_email" }, { "id": "get_email", "type": "collect", "config": { "variable": "email", "prompt": "What's your email?", "validation": { "type": "email" }, "errorMessage": "That does not look like an email address. Try again." }, "nextStepId": "get_size" }, { "id": "get_size", "type": "collect", "config": { "variable": "size", "prompt": "How many employees? Reply 1-10, 11-50, 51-200 or 200+.", "validation": { "type": "choice", "options": ["1-10", "11-50", "51-200", "200+"] }, "errorMessage": "Please reply with 1-10, 11-50, 51-200 or 200+." }, "nextStepId": "save_lead" }, { "id": "save_lead", "type": "tool", "config": { "toolName": "create_lead", "params": { "name": "{{name}}", "email": "{{email}}", "companySize": "{{size}}" } }, "nextStepId": "confirm" }, { "id": "confirm", "type": "message", "config": { "text": "Thanks {{name}}! A team member will reach out within 24 hours." } } ] }

The choice step is what makes this worth a flow: size reaching your CRM is always one of four values, not whatever the customer typed.

Appointment Booking Flow

JSON
{ "name": "Appointment Booking", "trigger": { "type": "keyword", "keywords": ["book", "appointment", "schedule"] }, "enabled": true, "priority": 10, "steps": [ { "id": "intro", "type": "message", "config": { "text": "I'd be happy to help you book an appointment!" }, "nextStepId": "get_service" }, { "id": "get_service", "type": "collect", "config": { "variable": "service_type", "prompt": "What type of appointment? Reply consultation, follow-up or review.", "validation": { "type": "choice", "options": ["consultation", "follow-up", "review"] }, "errorMessage": "Please reply with consultation, follow-up or review." }, "nextStepId": "get_date" }, { "id": "get_date", "type": "collect", "config": { "variable": "preferred_date", "prompt": "What date works best?", "validation": { "type": "date" } }, "nextStepId": "check_slots" }, { "id": "check_slots", "type": "tool", "config": { "toolName": "get_available_slots", "params": { "date": "{{preferred_date}}", "service": "{{service_type}}" }, "storeResultAs": "slots" }, "nextStepId": "show_slots" }, { "id": "show_slots", "type": "message", "config": { "text": "Available times on {{preferred_date}}: {{slots}}" }, "nextStepId": "get_time" }, { "id": "get_time", "type": "collect", "config": { "variable": "selected_time", "prompt": "Which time works for you?" }, "nextStepId": "book" }, { "id": "book", "type": "tool", "config": { "toolName": "create_appointment", "params": { "service": "{{service_type}}", "date": "{{preferred_date}}", "time": "{{selected_time}}" } }, "nextStepId": "confirm" }, { "id": "confirm", "type": "message", "config": { "text": "Your {{service_type}} appointment is booked for {{preferred_date}} at {{selected_time}}. See you then!" } } ] }

Both toolName values above must match tools that already exist: create_lead, get_available_slots and create_appointment must already exist on the agent.

Flow Sessions

When a user enters a flow, a session is created to track their progress. Sessions store:

  • Current step
  • Collected variables
  • Timestamps
  • Channel information

Best Practices

Keep Flows Short

Aim for 5-7 steps maximum. Long flows have higher abandonment rates.

Validate Early

Validate critical inputs (email, phone) immediately after collection.

Provide Exit Points

Let users say "stop" or "cancel" to exit the flow at any time.

Use Variables

Reference collected data in messages to personalize the experience.

Next Steps