Skip to main content
PreviewAgent Builder is currently in preview and may change before general availability.
In this tutorial, you build a mortgage advisor chatbot — a conversational AI app that guides users through mortgage product selection. The app detects what the user wants, answers questions from a knowledge base, collects financial data, and generates personalized recommendations using a combination of AI and deterministic business rules. What you will build:
  • A main orchestration workflow that uses an Intent Classification Agent to route messages
  • A knowledge base Q&A handler that answers mortgage questions from uploaded documents
  • A data collection handler that extracts and stores user financial data
  • A personalized offer generator using hybrid AI extraction + business rule calculations
  • A small talk responder and fallback handler
AI node types used: Intent Classification Agent, Text Generation, Custom Agent (with Knowledge Base) Patterns demonstrated: Intent classification, Knowledge base RAG, Hybrid AI + business rules

Architecture overview

The app uses an Intent Classification Agent node to classify each user message and route it to the right handler. Each intent maps to a separate output branch on the node, eliminating the need for a separate Condition node.

Data model

Each workflow has its own data model. Define these keys in the corresponding workflow.

mainChat data model

KeyTypeDescription
humanMessageSTRINGCurrent user message from chat
chatSessionIdSTRINGUnique session identifier
actionSTRINGChat action type (e.g., SEND_MESSAGE)
uiFlowSessionIdSTRINGUI flow session identifier
agentMessageSTRINGFinal response to send to chat UI
responseKeyOBJECTIntermediate response from handler subworkflows

answerPersonalisedOffer data model

KeyTypeDescription
clientProfileOBJECTClient financial data (extracted by AI from message)
clientProfile.ageNUMBERUser age
clientProfile.incomeNUMBERMonthly income
clientProfile.loan_amountNUMBERRequested loan amount
clientProfile.loan_durationNUMBERLoan term in years
filteredProductsOBJECTOutput from AI product filtering
calculationResultsOBJECTOutput from business rule calculations (DTI, max loan, PMT)
rankedProductsOBJECT[]Scored and ranked product recommendations
agentMessageSTRINGGenerated report text (set as Output Parameter)
Set agentMessage as an Output Parameter in the answerPersonalisedOffer data model. This is what flows back to mainChat through the subworkflow node.
Data does not persist across conversation turns in workflows. Each message triggers a fresh workflow execution. The personalized offer handler must extract all needed data from the current message, not from previous turns.

Prerequisites

Before starting, make sure you have:
  • Access to a FlowX Designer workspace with AI Platform enabled
  • Familiarity with creating workflows in FlowX
  • A Knowledge Base data source with mortgage-related documents uploaded (see Step 3)

Step 1: Build the main orchestration workflow

Create a workflow named mainChat. This is the central workflow that receives every user message, classifies it using an Intent Classification Agent, and routes it to the appropriate handler.
mainChat workflow showing Intent Classification Agent with branches to handlers

Add the Intent Classification Agent

From the node palette, drag an Intent Classification Agent node (under AI Agents) onto the canvas and connect it to the Start node. Configure the node: User Message: ${humanMessage} Intents:
#Intent description
1Greetings and small talk
2Product offer or mortgage recommendation request
3Knowledge base questions about mortgage products or policies
4User providing or updating personal data like income, age, or loan details
Response Key: responseKey If No Intent Matches: enabled (creates a fallback output branch)
Each intent creates a separate output port on the node. When the agent classifies a message, the workflow continues along the matching branch — no Condition node needed.

Connect handler nodes to each branch

Add the following nodes and connect each to its corresponding intent output:
Intent branchHandler nodeType
Intent 1 (Greetings)answerSmalltalkSubworkflow (Response Key: responseKey)
Intent 2 (Offer)answerPersonalisedOfferSubworkflow (Response Key: agentMessage)
Intent 3 (Knowledge QA)knowledgeBaseQASubworkflow (Response Key: responseKey)
Intent 4 (Data Input)handleDataInputScript node (inline)
No Matchanswer nonsenseScript node (inline)

Add the response formatter

After all branches, add a Script node (JavaScript) that all handler outputs converge into. This node normalizes the response from different handler types — some return responseKey.output.response (text), while the offer handler returns agentMessage directly.
var msg = "";
// Check offer branch (agentMessage from answerPersonalisedOffer)
if (input.agentMessage) {
  msg = input.agentMessage;
  if (typeof msg === "object" && msg.agentMessage) {
    msg = msg.agentMessage;
  }
  if (typeof msg === "object" && msg.agentMessage) {
    msg = msg.agentMessage;
  }
}
// Check other branches (responseKey from smalltalk, KB, etc.)
if (!msg || msg === "") {
  var rk = input.responseKey;
  if (rk && rk.output && rk.output.response) {
    msg = rk.output.response;
  }
}
output.agentMessage = msg || "No response generated.";
After making changes to any Script node, use Clear Cache on the workflow before testing. The platform caches compiled scripts, and changes may not take effect without clearing the cache first.

Add the End Flow node

Connect the process text node to an End Flow node with body:
{
  "message": {
    "chatSessionId": "${chatSessionId}",
    "agentMessage": "${agentMessage}"
  }
}
Configure the Start node body to declare the input keys:
{
  "humanMessage": "",
  "action": "",
  "chatSessionId": ""
}

Step 2: Build the handler workflows

answerSmalltalk (Text Generation)

Create a workflow named answerSmalltalk with a single Text Generation node. Operation Prompt:
Role: You are a friendly and professional Mortgage Assistant. Your goal
is to respond to greetings or casual conversation in a warm, trustworthy
manner, and subtly steer the conversation back toward the user's
mortgage goals.

Conversation history:
{{conversationHistory.messages}}

User message:
{{humanMessage}}

Response Guidelines:
- Tone: Warm, professional, and pragmatic.
- Acknowledge & Validate: Respond directly to what the user said.
- Subtle Pivot: End your response with a gentle nudge toward the
  mortgage process, without being pushy.
- Constraint: Keep the response under 3 sentences.
Response Key: responseKey Each handler workflow needs an End Flow node with body: {"output": ${responseKey}}

Fallback handler (Script)

For the No Match branch in the mainChat workflow, use a Script node named answer nonsense:
agentMessage = "Sorry, I cannot answer to your question."
responseKey = {}
responseKey["output"] = {}
responseKey["output"]["response"] = agentMessage
output['responseKey'] = responseKey

Step 3: Build the data input handler

This handler is a Script node named handleDataInput directly in the mainChat workflow (on the Intent 4 branch). It processes messages where the user provides financial data. Since the Intent Classification Agent already routed the message here, we know the user is providing personal data. The script uses simple string matching to extract values from the message.
The FlowX script runtime uses a subset of Python. Avoid enumerate(), .get() with defaults, and import statements — use basic loops and in checks instead.
Script node (Python):
message = ""
if "humanMessage" in input:
    message = input["humanMessage"]
clientProfile = {}
if "clientProfile" in input:
    clientProfile = input["clientProfile"]
words = message.lower().split()
numWords = len(words)
i = 0
while i < numWords:
    word = words[i]
    if word in ["years", "year"] and i > 0:
        prev = words[i - 1]
        if prev.isdigit():
            val = int(prev)
            if i < numWords - 1 and words[i + 1] == "old":
                if val > 15 and val < 120:
                    clientProfile["age"] = val
            else:
                if val >= 1 and val <= 50:
                    clientProfile["loan_duration"] = val
    if word == "age" and i < numWords - 1:
        nxt = words[i + 1]
        if nxt.isdigit() and int(nxt) > 15 and int(nxt) < 120:
            clientProfile["age"] = int(nxt)
    i = i + 1
i = 0
while i < numWords:
    word = words[i]
    if word in ["income", "salary", "earn", "earning", "make"]:
        j = i + 1
        while j < numWords and j < i + 4:
            val = words[j]
            if val.isdigit() and int(val) > 100:
                clientProfile["income"] = int(val)
            j = j + 1
    i = i + 1
i = 0
while i < numWords:
    word = words[i]
    if word in ["loan", "borrow", "mortgage"]:
        j = i + 1
        while j < numWords and j < i + 5:
            val = words[j]
            if val.isdigit() and int(val) > 1000:
                clientProfile["loan_amount"] = int(val)
            j = j + 1
    i = i + 1
collected = []
keys = list(clientProfile.keys())
k = 0
while k < len(keys):
    key = keys[k]
    collected.append(str(key) + ": " + str(clientProfile[key]))
    k = k + 1
agentMessage = ""
if len(collected) > 0:
    agentMessage = "Got it! I have recorded: " + ", ".join(collected) + ". What else can I help you with?"
else:
    agentMessage = "I could not extract data. Please tell me your age, income, loan amount, or loan duration."
responseKey = {}
responseKey["output"] = {}
responseKey["output"]["response"] = agentMessage
output["responseKey"] = responseKey
output["clientProfile"] = clientProfile
The age/duration disambiguation checks whether the word after a number + “years” is “old”. For example, “30 years old” sets age = 30, while “25 years” sets loan_duration = 25.
This handler makes the chatbot functional for data collection. Without it, users providing financial data would hit the fallback.

Step 4: Build the knowledge base QA handler

Create a workflow named knowledgeBaseQA with a single Custom Agent node connected to a Knowledge Base.

Set up the Knowledge Base

1

Create a Knowledge Base data source

In the Integration Designer, add a new Knowledge Base data source. Name it something descriptive like MortgageKnowledgeBase.
2

Upload mortgage documents

Upload PDF documents covering:
  • Mortgage product sheets (rates, terms, eligibility criteria)
  • FAQ documents (common questions about mortgages, DTI, LTV)
  • Regulatory guides (required documents, application process)
Wait for automatic chunking and vector indexing to complete.
3

Test queries

Use the Knowledge Base test interface to verify that queries like “What is DTI?” and “What documents do I need?” return relevant chunks.
For detailed Knowledge Base setup, see the Knowledge Base integration documentation.

Configure the Custom Agent node

In the knowledgeBaseQA workflow, add a Custom Agent node and enable the Knowledge Base tool. Select your MortgageKnowledgeBase data source. Retrieval parameters:
ParameterValue
Max. Number of Results4
Min. Relevance Score50
Content Source FilterAll content sources
System Prompt:
Role: You are a Mortgage Knowledge Base Expert who uses an integrated
Knowledge Base (via Qdrant vector search) to provide accurate,
data-grounded answers.

Conversation history: {{conversationHistory.messages}}

User question: {{humanMessage}}

Operating Procedure:
1. Query Analysis: Identify key terms from the user's question.
2. Qdrant Search: Find the most relevant document fragments.
3. Response Synthesis: Formulate a clear answer using ONLY the
   information found.

Strict Rules:
- If the information is not in the Knowledge Base, respond:
  "Our current knowledge base does not contain specific information
  about this topic."
- Reference the source document when possible.
- Never fabricate rates, terms, or requirements.
- Keep responses concise (under 200 words).
Response Key: responseKey
Without explicit grounding rules in the prompt, the LLM may fall back to its general training data and produce inaccurate mortgage information. Always include the “ONLY from knowledge base” instruction.
For more details on this pattern, see Knowledge base RAG.

Step 5: Build the personalized offer handler

This is the most complex handler. It implements the Hybrid AI + business rules pattern — alternating between AI nodes and deterministic Script nodes. Create a workflow named answerPersonalisedOffer with the following pipeline:
answerPersonalisedOffer workflow showing Text Understanding, financialCalculations, scoringRanking, and End Flow nodes
Start
  → Text Understanding (extract client data + filter products)
    → Script (financial calculations: PMT, DTI, max loan)
      → Script (score, rank, and generate report)
        → End Flow
The Text Understanding node extracts client data directly from {{humanMessage}} and evaluates products in a single step. This makes the subworkflow self-contained — it does not depend on data collected in previous conversation turns.

Step 5a: AI understanding (extract and filter)

Add a Text Understanding node that extracts the client’s financial profile from their message and filters the product catalog. Operation Prompt:
You are a mortgage product filter. Extract the client's financial
details from their message and evaluate available products.

Client message: {{humanMessage}}

Available products:
1. fixed30 (Alpha Bank): 4.5% fixed, 30 years, min income 3000 EUR,
   max LTV 80%, min age 21, max age at maturity 70, prepayment after
   5 years, 20% down payment
2. variable20 (Beta Bank): 3.5% variable (EURIBOR 6M + 1.8%), 20 years,
   min income 2500 EUR, max LTV 85%, min age 23, max age at maturity 65,
   no prepayment first 3 years, 15% down payment
3. fixed15 (Gamma Bank): 4.0% fixed, 15 years, min income 4000 EUR,
   max LTV 75%, min age 25, max age at maturity 60, prepayment anytime,
   25% down payment

First extract from the message:
- age (number)
- monthly_income (number)
- loan_amount (number)
- loan_duration (number, in years)

Then for each product, evaluate eligibility and return JSON only:
{
  "client": {"age": 0, "monthly_income": 0, "loan_amount": 0,
             "loan_duration": 0},
  "filtered_products": [
    {"product_id": "fixed30", "qualitative_match_score": 0.8,
     "match_reasons": "brief explanation", "disqualified": false}
  ]
}

Only include products where disqualified is false. Return ONLY valid
JSON, no other text.
Response Schema:
{
  "type": "object",
  "properties": {
    "client": {
      "type": "object",
      "properties": {
        "age": { "type": "number" },
        "monthly_income": { "type": "number" },
        "loan_amount": { "type": "number" },
        "loan_duration": { "type": "number" }
      }
    },
    "filtered_products": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "product_id": { "type": "string" },
          "qualitative_match_score": { "type": "number" },
          "match_reasons": { "type": "string" },
          "disqualified": { "type": "boolean" }
        }
      }
    }
  }
}
Response Key: filteredProducts

Step 5b: Business rules (financial calculations)

Add a Script node (JavaScript) for deterministic financial calculations. These must be auditable and reproducible.
AI node output in Script nodes arrives as Java HashMaps, not JavaScript objects. Use .get("key") to access properties and .size() / .get(index) for lists. Standard JavaScript methods like Object.keys() and JSON.stringify() do not work on these objects.
var fp = input.filteredProducts;
var client = fp.get("client");

var income = parseFloat(client.get("monthly_income")) || 0;
var loanAmount = parseFloat(client.get("loan_amount")) || 0;
var loanDuration = parseInt(client.get("loan_duration")) || 20;
var termMonths = loanDuration * 12;

function computePMT(principal, annualRate, months) {
  var r = annualRate / 12;
  if (r === 0) return principal / months;
  return principal * (r * Math.pow(1 + r, months)) /
          (Math.pow(1 + r, months) - 1);
}

var estimatedRate = 0.045;
var monthlyPayment = income > 0
  ? computePMT(loanAmount, estimatedRate, termMonths) : 0;
var dti = income > 0 ? monthlyPayment / income : 1;

var maxDtiShare = 0.43;
var allowablePayment = maxDtiShare * income;
var maxLoan = allowablePayment > 0
  ? allowablePayment * ((Math.pow(1 + estimatedRate / 12, termMonths) - 1) /
    (estimatedRate / 12 * Math.pow(1 + estimatedRate / 12, termMonths)))
  : 0;

output.calculationResults = {
  monthlyPayment: Math.round(monthlyPayment * 100) / 100,
  dti: Math.round(dti * 10000) / 10000,
  maxLoanAmount: Math.round(maxLoan),
  meetsEligibility: dti <= maxDtiShare,
  requestedVsMax: loanAmount <= maxLoan ? "within_limits" : "exceeds_limits"
};
The alternating AI-then-rules structure creates a natural audit trail. For any final recommendation, you can trace exactly which AI filtered the products and which formula computed the financial results.

Step 5c: Business rules (scoring, ranking, and report)

Add a second Script node (JavaScript) that scores products, ranks them, and generates the recommendation report text. This node also writes to agentMessage, which is the Output Parameter that flows back to mainChat.
var calcResults = input.calculationResults || {};

var catalog = [
  {id: "fixed30", bankName: "Alpha Bank", rate: 0.045,
   allowsPrepayment: true, flexibleTerm: false, term: 30},
  {id: "variable20", bankName: "Beta Bank", rate: 0.035,
   allowsPrepayment: false, flexibleTerm: true, term: 20},
  {id: "fixed15", bankName: "Gamma Bank", rate: 0.04,
   allowsPrepayment: true, flexibleTerm: true, term: 15}
];

var fp = input.filteredProducts;
var aiProducts = fp.get("filtered_products");
var aiFiltered = [];
for (var i = 0; i < aiProducts.size(); i++) {
  var p = aiProducts.get(i);
  aiFiltered.push({
    product_id: "" + p.get("product_id"),
    qualitative_match_score:
      parseFloat(p.get("qualitative_match_score")) || 0.7,
    match_reasons: "" + p.get("match_reasons")
  });
}

var WEIGHT_RATE = 0.35;
var WEIGHT_QUALITATIVE = 0.25;
var WEIGHT_LTV = 0.20;
var WEIGHT_FLEXIBILITY = 0.20;

var scored = [];
for (var i = 0; i < aiFiltered.length; i++) {
  var product = aiFiltered[i];
  var details = null;
  for (var j = 0; j < catalog.length; j++) {
    if (catalog[j].id === product.product_id) {
      details = catalog[j];
      break;
    }
  }
  if (!details) continue;
  var rateScore = 1 - (details.rate / 0.10);
  var flexScore = (details.allowsPrepayment ? 0.5 : 0) +
                  (details.flexibleTerm ? 0.5 : 0);
  var totalScore = (rateScore * WEIGHT_RATE) +
    (product.qualitative_match_score * WEIGHT_QUALITATIVE) +
    (0.8 * WEIGHT_LTV) +
    (flexScore * WEIGHT_FLEXIBILITY);
  scored.push({
    productId: product.product_id,
    bankName: details.bankName,
    rate: details.rate,
    term: details.term,
    totalScore: Math.round(totalScore * 1000) / 1000,
    matchReasons: product.match_reasons
  });
}

scored.sort(function(a, b) { return b.totalScore - a.totalScore; });
var top = scored.slice(0, 3);

var mp = calcResults.monthlyPayment || 0;
var dti = calcResults.dti || 0;
var maxLoan = calcResults.maxLoanAmount || 0;
var eligible = calcResults.meetsEligibility !== false;

var report = "Mortgage Recommendation Report. ";
report += "Eligibility Assessment: ";
report += "Estimated monthly payment: " + mp + " EUR. ";
report += "Debt-to-income ratio: " + (dti * 100).toFixed(1) + "%. ";
report += "Maximum eligible loan: " + maxLoan + " EUR. ";
report += "Status: " + (eligible ? "Eligible" : "May need adjustment")
  + ". ";
report += "Top Recommendations: ";

for (var k = 0; k < top.length; k++) {
  var p = top[k];
  report += (k + 1) + ". " + p.bankName + " (" + p.productId + ") - "
    + (p.rate * 100) + "% for " + p.term + " years. ";
  report += "Score: " + p.totalScore + ". " + p.matchReasons + ". ";
}

report += "Next Steps: Compare the options above, gather required "
  + "documents, and schedule a consultation with your preferred bank.";

output.agentMessage = report;
output.rankedProducts = top;
The report is generated as plain text in the Script node rather than using a separate Text Generation node. This avoids issues with newline characters in the End Flow node’s JSON serialization. For richer formatting, you can use a Text Generation node and strip newlines before returning.

Step 6: Connect to the chat UI

1

Create a UI Flow

Go to UI Flows in the project sidebar and create a new UI Flow (e.g., chat). Add a Page to it.
2

Add a Chat component

From the component palette, drag a Chat component onto the page. In the Chat component settings, set the Workflow property to mainChat.
3

Test the chat

Click Run to preview the UI Flow and interact with the chatbot.
For details on configuring chat experiences with built-in session memory, see Conversational workflows. For the Chat component reference, see Chat component.

Testing

1

Test knowledge base Q&A

Open the knowledgeBaseQA workflow and use Run Workflow with:
{ "humanMessage": "What is a debt-to-income ratio?" }
Verify the response is grounded in your uploaded documents, not generic LLM knowledge.
2

Test the full chat flow

Run the mainChat workflow via the Chat UI. Since data does not persist across conversation turns, include all financial details in a single message when requesting an offer:
TurnMessageExpected behavior
1”Hi there!”Greeting response, nudge toward mortgage
2”I am 30 years old and my income is 5000”Acknowledges data (age, income recorded)
3”My age is 30, income 5000 EUR per month. I want a mortgage of 200000 EUR for 25 years. Give me product recommendations.”Recommendation report with eligibility assessment and ranked products
Chat UI showing mortgage recommendation report with eligibility assessment and product recommendations
3

Test edge cases

Test messageExpected intent branchExpected behavior
”What documents do I need to apply?”Knowledge QAAnswer from knowledge base
”asdfghjk”No MatchFallback response
”I changed my mind, my income is 6000”Data InputUpdates income, confirms change

What you learned

In this tutorial, you built a full-featured conversational AI app that demonstrates:
  • Intent classification and routing — using an Intent Classification Agent to classify messages and route to handler branches automatically (pattern)
  • Knowledge base RAG — grounding answers in uploaded documents using a Custom Agent with Knowledge Base (pattern)
  • Hybrid AI + business rules — combining AI qualitative filtering with deterministic financial calculations for auditable recommendations (pattern)
  • Workflow composition — using Subworkflow nodes to build modular, reusable AI pipelines

Next steps

Last modified on March 16, 2026