Skip to content

Flow Agents

Flow agents divide the conversation into nodes — each node has its own prompt, tools, and transition logic. The LLM calls transition functions to move the conversation forward.


Contents


How Flows Work

When the engine enters a node:

  1. A new system prompt is assembled: global prompt + role_messages + task_messages
  2. Available tools are replaced with this node's functions + tool_ids
  3. pre_actions run silently before the LLM speaks (database lookups, etc.)
  4. The LLM responds and can call functions to advance the flow

After the LLM calls a transition function, the engine:

  1. Locks further transitions until the caller speaks (prevents chaining without user input)
  2. Swaps the system prompt and tools to the next node
  3. The LLM responds again from the new node's context

Node Fields

FieldTypeDescription
node_keystringUnique ID within this agent's flow. e.g. "greeting", "collect_name"
positionintDisplay order in the visual editor
is_initialboolEntry point. Exactly one node must be true
is_terminalboolThe call can end here. The LLM gets end_call automatically
role_messageslistWho the agent is. Same across all nodes. Injected first
task_messageslistWhat the agent must do at this specific step
functionslistTransition functions to move to other nodes
tool_idslistWebhook tool UUIDs available at this node
builtin_toolslistPlatform tools: ["end_call"]
pre_actionslistTools that fire automatically on node entry
allow_interruptboolWhether the caller can interrupt the agent mid-speech. Default true
position_xyobject{"x": 100, "y": 200} — visual editor canvas position only

Writing Good Nodes

role_messages — defines persona. Put on the initial node, or every node where you want to reinforce identity:

json
{
  "role": "system",
  "content": "You are Priya, a warm and professional appointment coordinator at Dr. Sharma's clinic."
}

task_messages — defines the task at this step. Be specific:

  • What to ask
  • What information to collect
  • When to call which function
  • How to handle edge cases
json
{
  "role": "system",
  "content": "Ask the caller for their full name and preferred date for the appointment. Use the check_slots tool to confirm availability. Once they choose a slot, call details_confirmed with their name and the chosen slot."
}

Common mistake: Vague task messages lead to the LLM making up its own logic. The more precise the instruction, the more reliable the flow.


Transition Functions

Functions are how the LLM moves between nodes. They appear as callable functions in the LLM's context.

json
{
  "name": "proceed_to_booking",
  "description": "Call this once the caller confirms they want to book an appointment.",
  "properties": {
    "appointment_type": {
      "type": "string",
      "enum": ["checkup", "consultation", "follow_up"],
      "description": "Type of appointment"
    }
  },
  "required": ["appointment_type"],
  "next_node_key": "collect_details"
}

Function fields:

FieldRequiredDescription
nameYesFunction name. Use descriptive verbs: route_to_billing, record_needs, qualify_lead
descriptionYesWhen the LLM should call this. Be specific about the trigger condition
propertiesNoParameters the LLM fills in. Same format as tool parameters
requiredNoRequired parameters
next_node_keyYesThe node_key to transition to

Writing good descriptions:

Good: "Call this after the caller gives their budget and preferred location."
Bad:  "Go to next step"

Good: "Call this if the caller is not available and wants to be called back later."
Bad:  "Callback"

Branching

A node can have multiple functions leading to different nodes. The LLM picks based on the conversation:

json
"functions": [
  {
    "name": "qualify_lead",
    "description": "Caller has budget, timeline within 6 months, and financing sorted. Call this to proceed.",
    "next_node_key": "qualified"
  },
  {
    "name": "disqualify_lead",
    "description": "Caller is just exploring, has no budget, or timeline is beyond 12 months.",
    "properties": {
      "reason": { "type": "string", "description": "Why they did not qualify" }
    },
    "next_node_key": "not_qualified"
  }
]

Add clear criteria to task_messages so the LLM knows which to pick:

json
{
  "role": "system",
  "content": "Qualify the lead based on:\n- QUALIFIED: has budget 50L+, timeline within 6 months, has financing. Call qualify_lead.\n- NOT QUALIFIED: just exploring, no budget, or timeline > 12 months. Call disqualify_lead."
}

Terminal Nodes

A terminal node has is_terminal: true. The platform:

  1. Automatically gives the LLM the end_call function
  2. Appends a strong instruction to call end_call after the farewell message
  3. Starts a 10-second safety net — if the LLM hasn't called end_call after 10 seconds, the platform ends the call automatically

Always instruct the LLM to call end_call in the terminal node's task_messages:

json
{
  "node_key": "farewell",
  "is_terminal": true,
  "task_messages": [
    {
      "role": "system",
      "content": "Thank the caller for their time and wish them a good day. Then call end_call immediately."
    }
  ],
  "functions": [],
  "tool_ids": [],
  "builtin_tools": ["end_call"]
}

Escape Hatches

Real callers don't follow scripts. A caller might say "call me later" while the agent is mid-flow. Without an escape hatch on the current node, the LLM gets stuck — it has no function to handle that response.

Fix — add escape hatch functions to every non-terminal node:

json
{
  "node_key": "collect_profile",
  "functions": [
    { "name": "profile_complete",    "next_node_key": "book_meeting" },
    { "name": "lead_not_a_fit",      "next_node_key": "disqualified_exit" },

    {
      "name": "caller_wants_callback",
      "description": "Call this if the caller says they are busy or wants to be called back later",
      "properties": {
        "callback_time": { "type": "string", "description": "When to call back" }
      },
      "required": [],
      "next_node_key": "handle_callback"
    },
    {
      "name": "caller_not_interested",
      "description": "Call this if the caller says they are not interested",
      "properties": {},
      "required": [],
      "next_node_key": "farewell"
    }
  ],
  "builtin_tools": ["end_call"]
}

Recommendation: Add "builtin_tools": ["end_call"] to every node — terminal and non-terminal. This gives the LLM an escape at every stage.


Example 1 — Appointment Booking

Nodes: greetingcollect_detailsconfirm_slotfarewell

json
{
  "version": "1",
  "agent": {
    "name": "appointment-bot",
    "prompt": "You are an appointment scheduling assistant for Dr. Sharma's clinic.",
    "greeting": "Hello! I'm calling from Dr. Sharma's clinic. Is now a good time to book your appointment?"
  },
  "tools": [
    {
      "id": "tool-check-slots",
      "name": "check_available_slots",
      "description": "Check available appointment slots for a given date. Returns a list of open times.",
      "webhook_url": "https://your-api.com/slots",
      "webhook_method": "POST",
      "parameters": {
        "properties": {
          "date": { "type": "string", "description": "Date to check (YYYY-MM-DD)" }
        },
        "required": ["date"]
      }
    },
    {
      "id": "tool-book",
      "name": "book_appointment",
      "description": "Book an appointment for a patient. Returns confirmation number.",
      "webhook_url": "https://your-api.com/book",
      "webhook_method": "POST",
      "parameters": {
        "properties": {
          "patient_name": { "type": "string", "description": "Patient's full name" },
          "slot":         { "type": "string", "description": "Chosen slot (ISO datetime)" },
          "phone_number": { "type": "string", "description": "Patient's phone number" }
        },
        "required": ["patient_name", "slot", "phone_number"]
      }
    }
  ],
  "flow_nodes": [
    {
      "node_key": "greeting",
      "position": 0,
      "is_initial": true,
      "is_terminal": false,
      "role_messages": [
        { "role": "system", "content": "You are a warm and professional appointment coordinator at Dr. Sharma's clinic." }
      ],
      "task_messages": [
        { "role": "system", "content": "Ask if the caller is available to talk right now. If yes, call caller_available. If no or busy, call caller_busy." }
      ],
      "functions": [
        {
          "name": "caller_available",
          "description": "Call when the caller confirms they have time to talk",
          "next_node_key": "collect_details",
          "properties": {},
          "required": []
        },
        {
          "name": "caller_busy",
          "description": "Call when the caller says they are busy or not available right now",
          "next_node_key": "farewell",
          "properties": {},
          "required": []
        }
      ],
      "tool_ids": [],
      "builtin_tools": ["end_call"],
      "pre_actions": [],
      "position_xy": { "x": 100, "y": 200 }
    },
    {
      "node_key": "collect_details",
      "position": 1,
      "is_initial": false,
      "is_terminal": false,
      "role_messages": [],
      "task_messages": [
        { "role": "system", "content": "Ask for the caller's name and preferred date for the appointment. Use check_available_slots to confirm availability for their preferred date. Present the available times. Once they confirm a slot, call details_confirmed with their name and the chosen slot." }
      ],
      "functions": [
        {
          "name": "details_confirmed",
          "description": "Call after the caller confirms their name and preferred appointment slot",
          "next_node_key": "confirm_slot",
          "properties": {
            "patient_name": { "type": "string", "description": "Caller's full name" },
            "slot":         { "type": "string", "description": "Confirmed appointment slot (ISO datetime)" }
          },
          "required": ["patient_name", "slot"]
        },
        {
          "name": "caller_wants_callback",
          "description": "Call if the caller wants to be contacted at a different time",
          "next_node_key": "farewell",
          "properties": {},
          "required": []
        }
      ],
      "tool_ids": ["tool-check-slots"],
      "builtin_tools": ["end_call"],
      "pre_actions": [],
      "position_xy": { "x": 350, "y": 200 }
    },
    {
      "node_key": "confirm_slot",
      "position": 2,
      "is_initial": false,
      "is_terminal": false,
      "role_messages": [],
      "task_messages": [
        { "role": "system", "content": "The booking is being made. Once you enter this node, the booking was already placed (check pre_actions result). Confirm the appointment details with the caller including date, time, and confirmation number from the pre_action result. Then call confirmed." }
      ],
      "functions": [
        {
          "name": "confirmed",
          "description": "Call after reading out the appointment confirmation to the caller",
          "next_node_key": "farewell",
          "properties": {},
          "required": []
        }
      ],
      "tool_ids": [],
      "builtin_tools": ["end_call"],
      "pre_actions": [
        { "type": "tool_call", "tool_id": "tool-book" }
      ],
      "position_xy": { "x": 600, "y": 200 }
    },
    {
      "node_key": "farewell",
      "position": 3,
      "is_initial": false,
      "is_terminal": true,
      "role_messages": [],
      "task_messages": [
        { "role": "system", "content": "Thank the caller warmly. If an appointment was booked, remind them of the date. Wish them a good day. Then call end_call." }
      ],
      "functions": [],
      "tool_ids": [],
      "builtin_tools": ["end_call"],
      "pre_actions": [],
      "position_xy": { "x": 850, "y": 200 }
    }
  ]
}

Flow diagram:

[greeting] ──available──► [collect_details] ──confirmed──► [confirm_slot] ──► [farewell]
     │                           │                                               ▲
     └──busy──────────────────────┴──wants_callback──────────────────────────────┘

Example 2 — Lead Qualification (Real Estate)

Outbound agent qualifies prospects before routing to a sales team. Nodes: greetingqualifyschedule_visitnot_qualifiedfarewell.

json
{
  "version": "1",
  "agent": {
    "name": "realty-qualifier",
    "prompt": "You are Aisha, a property consultant from HomeNest Realty. You are warm, professional, and concise.",
    "greeting": "Hello {{customer_name}}! This is Aisha from HomeNest Realty calling about properties in {{area}}. Do you have a moment?",
    "context_variables": {
      "customer_name": { "type": "string", "description": "Prospect's name" },
      "area":          { "type": "string", "description": "Area they enquired about" }
    }
  },
  "tools": [
    {
      "id": "tool-schedule-visit",
      "name": "schedule_site_visit",
      "description": "Schedule a site visit for a qualified prospect. Returns a booking confirmation.",
      "webhook_url": "https://your-api.com/visits/schedule",
      "webhook_method": "POST",
      "parameters": {
        "properties": {
          "customer_name":  { "type": "string" },
          "phone_number":   { "type": "string" },
          "preferred_date": { "type": "string" },
          "preferred_time": { "type": "string" }
        },
        "required": ["customer_name", "phone_number", "preferred_date"]
      }
    },
    {
      "id": "tool-log-lead",
      "name": "log_lead_outcome",
      "description": "Log the outcome of this lead call to CRM.",
      "webhook_url": "https://your-api.com/crm/lead",
      "webhook_method": "POST",
      "parameters": {
        "properties": {
          "phone_number": { "type": "string" },
          "outcome":      { "type": "string", "enum": ["qualified", "not_qualified", "callback", "not_interested"] },
          "notes":        { "type": "string" }
        },
        "required": ["phone_number", "outcome"]
      }
    }
  ],
  "flow_nodes": [
    {
      "node_key": "greeting",
      "position": 0,
      "is_initial": true,
      "is_terminal": false,
      "role_messages": [
        { "role": "system", "content": "You are Aisha, a warm property consultant from HomeNest Realty." }
      ],
      "task_messages": [
        { "role": "system", "content": "Greet {{customer_name}} and ask if now is a good time. If yes, call proceed_to_qualify. If busy, ask for a good time, call log_lead_outcome with outcome=callback, then proceed to farewell." }
      ],
      "functions": [
        {
          "name": "proceed_to_qualify",
          "description": "Caller is available to talk",
          "next_node_key": "qualify",
          "properties": {},
          "required": []
        },
        {
          "name": "caller_busy",
          "description": "Caller is busy or not available right now",
          "next_node_key": "farewell",
          "properties": {},
          "required": []
        }
      ],
      "tool_ids": ["tool-log-lead"],
      "builtin_tools": ["end_call"],
      "pre_actions": [],
      "position_xy": { "x": 100, "y": 200 }
    },
    {
      "node_key": "qualify",
      "position": 1,
      "is_initial": false,
      "is_terminal": false,
      "role_messages": [],
      "task_messages": [
        {
          "role": "system",
          "content": "Qualify the lead with 3 questions (ask naturally, not as a list):\n1. What is their budget range?\n2. Are they looking to buy in the next 3–6 months?\n3. Have they sorted financing (pre-approved, self-funded, or planning to apply)?\n\nQUALIFIED: budget 50L+, timeline within 6 months, financing in place → call qualify_lead\nNOT QUALIFIED: just exploring, budget unclear, or timeline > 12 months → call disqualify_lead\n\nIf they go off-topic or resist: use escape hatch functions."
        }
      ],
      "functions": [
        {
          "name": "qualify_lead",
          "description": "Lead qualifies — has budget, near-term timeline, and financing sorted",
          "next_node_key": "schedule_visit",
          "properties": {
            "budget":    { "type": "string", "description": "Budget range mentioned" },
            "financing": { "type": "string", "enum": ["pre_approved", "self_funded", "planning_to_apply"] }
          },
          "required": ["financing"]
        },
        {
          "name": "disqualify_lead",
          "description": "Lead does not qualify — just exploring, no budget, or long timeline",
          "next_node_key": "not_qualified",
          "properties": {
            "reason": { "type": "string", "description": "Why they did not qualify" }
          },
          "required": []
        },
        {
          "name": "caller_not_interested",
          "description": "Caller explicitly says they are not interested",
          "next_node_key": "farewell",
          "properties": {},
          "required": []
        },
        {
          "name": "caller_wants_callback",
          "description": "Caller asks to be called back at another time",
          "next_node_key": "farewell",
          "properties": {},
          "required": []
        }
      ],
      "tool_ids": ["tool-log-lead"],
      "builtin_tools": ["end_call"],
      "pre_actions": [],
      "position_xy": { "x": 350, "y": 200 }
    },
    {
      "node_key": "schedule_visit",
      "position": 2,
      "is_initial": false,
      "is_terminal": false,
      "role_messages": [],
      "task_messages": [
        { "role": "system", "content": "Congratulate the caller on being a great fit for our properties. Ask for their preferred date and time for a site visit. Call schedule_site_visit with their details and confirm the booking. Then call visit_booked." }
      ],
      "functions": [
        {
          "name": "visit_booked",
          "description": "Site visit has been scheduled and confirmed to the caller",
          "next_node_key": "farewell",
          "properties": {},
          "required": []
        }
      ],
      "tool_ids": ["tool-schedule-visit"],
      "builtin_tools": ["end_call"],
      "pre_actions": [],
      "position_xy": { "x": 600, "y": 100 }
    },
    {
      "node_key": "not_qualified",
      "position": 3,
      "is_initial": false,
      "is_terminal": false,
      "role_messages": [],
      "task_messages": [
        { "role": "system", "content": "Tell the caller that HomeNest also has great options for people at different stages — mention that you can send them property updates via WhatsApp and reach out when the timing is right. Ask if that is okay. Log the outcome with log_lead_outcome (outcome=not_qualified). Then call wrap_up." }
      ],
      "functions": [
        {
          "name": "wrap_up",
          "description": "Caller is okay with updates, or conversation is wrapping up",
          "next_node_key": "farewell",
          "properties": {},
          "required": []
        }
      ],
      "tool_ids": ["tool-log-lead"],
      "builtin_tools": ["end_call"],
      "pre_actions": [],
      "position_xy": { "x": 600, "y": 300 }
    },
    {
      "node_key": "farewell",
      "position": 4,
      "is_initial": false,
      "is_terminal": true,
      "role_messages": [],
      "task_messages": [
        { "role": "system", "content": "Thank the caller warmly and wish them a good day. Then call end_call." }
      ],
      "functions": [],
      "tool_ids": [],
      "builtin_tools": ["end_call"],
      "pre_actions": [],
      "position_xy": { "x": 900, "y": 200 }
    }
  ]
}

Example 3 — Loan Collection (Outbound)

Outbound calls to collect overdue payments. Nodes: greetingverify_identitypresent_duesconfirm_paymentclose_call.

See json-import.md for the full JSON of this example.

Flow diagram:

[greeting]
    │ confirmed_correct_person

[verify_identity] ──identity_verified──► [present_dues]
    │                                         │
    │ identity_failed               agrees_to_pay │  wants_callback
    ▼                                         ▼         │
[close_call] ◄──────── customer_refuses ── [confirm_payment]

                                    arrangement_confirmed

                                          [close_call]

Example 4 — Post-Service Survey

Inbound agent that runs a structured satisfaction survey after a service appointment. Collects rating, specific feedback, and NPS score.

json
{
  "version": "1",
  "agent": {
    "name": "feedback-survey",
    "prompt": "You are a friendly survey agent for TechServ. You are calling to collect feedback about a recent service visit. Keep it conversational — never say 'question 1' or make it feel like a form.",
    "greeting": "Hi {{customer_name}}! This is TechServ calling. We recently completed a service visit for you and we'd love to get your feedback. It'll only take about 2 minutes — is that okay?",
    "context_variables": {
      "customer_name":   { "type": "string", "description": "Customer name" },
      "technician_name": { "type": "string", "description": "Technician who did the visit" },
      "service_type":    { "type": "string", "description": "Type of service done" }
    }
  },
  "tools": [
    {
      "id": "tool-save-feedback",
      "name": "save_feedback",
      "description": "Save the customer's survey responses. Call this at the end of the survey.",
      "webhook_url": "https://your-api.com/feedback",
      "webhook_method": "POST",
      "parameters": {
        "properties": {
          "phone_number":      { "type": "string" },
          "overall_rating":    { "type": "number", "description": "1-5 overall satisfaction rating" },
          "technician_rating": { "type": "number", "description": "1-5 rating for the technician" },
          "nps_score":         { "type": "number", "description": "0-10 Net Promoter Score" },
          "feedback_text":     { "type": "string", "description": "Open-ended feedback" }
        },
        "required": ["phone_number", "overall_rating"]
      }
    }
  ],
  "flow_nodes": [
    {
      "node_key": "consent",
      "position": 0,
      "is_initial": true,
      "is_terminal": false,
      "role_messages": [
        { "role": "system", "content": "You are a friendly and efficient survey agent for TechServ." }
      ],
      "task_messages": [
        { "role": "system", "content": "Ask if the customer is okay to give feedback now. If yes, call start_survey. If they are busy or decline, thank them and call end_without_survey." }
      ],
      "functions": [
        { "name": "start_survey",        "description": "Customer agrees to give feedback", "next_node_key": "overall_rating", "properties": {}, "required": [] },
        { "name": "end_without_survey",  "description": "Customer declines or is busy",       "next_node_key": "farewell",        "properties": {}, "required": [] }
      ],
      "tool_ids": [],
      "builtin_tools": ["end_call"],
      "pre_actions": [],
      "position_xy": { "x": 100, "y": 200 }
    },
    {
      "node_key": "overall_rating",
      "position": 1,
      "is_initial": false,
      "is_terminal": false,
      "role_messages": [],
      "task_messages": [
        { "role": "system", "content": "Ask: 'On a scale of 1 to 5, how satisfied were you with the overall service — 1 being very dissatisfied, 5 being very satisfied?' Accept any number 1–5. Then call rating_given with the number." }
      ],
      "functions": [
        {
          "name": "rating_given",
          "description": "Customer gave their overall rating",
          "next_node_key": "technician_rating",
          "properties": {
            "rating": { "type": "number", "description": "Overall rating 1-5" }
          },
          "required": ["rating"]
        }
      ],
      "tool_ids": [],
      "builtin_tools": ["end_call"],
      "pre_actions": [],
      "position_xy": { "x": 350, "y": 200 }
    },
    {
      "node_key": "technician_rating",
      "position": 2,
      "is_initial": false,
      "is_terminal": false,
      "role_messages": [],
      "task_messages": [
        { "role": "system", "content": "Ask how they would rate {{technician_name}} specifically — also on a scale of 1 to 5. Then call tech_rated." }
      ],
      "functions": [
        {
          "name": "tech_rated",
          "description": "Customer gave the technician a rating",
          "next_node_key": "open_feedback",
          "properties": {
            "rating": { "type": "number", "description": "Technician rating 1-5" }
          },
          "required": ["rating"]
        }
      ],
      "tool_ids": [],
      "builtin_tools": ["end_call"],
      "pre_actions": [],
      "position_xy": { "x": 600, "y": 200 }
    },
    {
      "node_key": "open_feedback",
      "position": 3,
      "is_initial": false,
      "is_terminal": false,
      "role_messages": [],
      "task_messages": [
        { "role": "system", "content": "Ask if there is anything specific they would like to share — what went well or what could be improved. Accept a few sentences. Then ask: 'On a scale of 0 to 10, how likely are you to recommend TechServ to a friend or colleague?' Once you have the NPS score, call feedback_complete with all the collected data." }
      ],
      "functions": [
        {
          "name": "feedback_complete",
          "description": "All survey questions answered — overall rating, tech rating, open feedback, and NPS collected",
          "next_node_key": "farewell",
          "properties": {
            "overall_rating":    { "type": "number" },
            "technician_rating": { "type": "number" },
            "feedback_text":     { "type": "string" },
            "nps_score":         { "type": "number" }
          },
          "required": ["overall_rating", "nps_score"]
        }
      ],
      "tool_ids": ["tool-save-feedback"],
      "builtin_tools": ["end_call"],
      "pre_actions": [],
      "position_xy": { "x": 850, "y": 200 }
    },
    {
      "node_key": "farewell",
      "position": 4,
      "is_initial": false,
      "is_terminal": true,
      "role_messages": [],
      "task_messages": [
        { "role": "system", "content": "Thank the customer for their time and feedback. Tell them it helps TechServ improve. Wish them a good day. Then call end_call." }
      ],
      "functions": [],
      "tool_ids": [],
      "builtin_tools": ["end_call"],
      "pre_actions": [],
      "position_xy": { "x": 1100, "y": 200 }
    }
  ]
}

Flow Design Checklist

  • [ ] Exactly one node has is_initial: true
  • [ ] At least one node has is_terminal: true (otherwise the agent can never end the call)
  • [ ] Every next_node_key in a function points to an existing node_key
  • [ ] Every tool_id in tool_ids and pre_actions is a valid UUID
  • [ ] Terminal nodes have functions: [] — the platform provides end_call automatically
  • [ ] Each node's task_messages clearly tells the LLM when to call each function
  • [ ] builtin_tools: ["end_call"] on every node (so LLM can hang up at any stage)
  • [ ] Escape hatch functions on every non-terminal node (callback, not interested)
  • [ ] Objection tools (schedule_callback, mark_do_not_call) in tool_ids on every node