Low Code Chatbot Building

Production-grade guide to low code chatbot building covering architecture patterns, implementation strategies, testing approaches, and operational best practices for enterprise engineering teams.

Low code chatbot building enables rapid creation of conversational interfaces using visual workflows, pre-built components, and declarative configuration—ideal when response time, integration density, and user experience are critical, and when teams lack dedicated software engineers. It matters when a product team needs to deploy a customer support bot, onboarding assistant, or internal helpdesk in under two weeks, without writing a single line of code.

Designing Conversation Flows

Start with a dialog canvas where each node is a turn in the conversation. Each turn has a message, a response type, and transition conditions. The canvas supports branching, loops, and parallel paths. Use dialog.start as the entry point and dialog.end to close the session.

Define Message Types and Content

Messages can be static text, dynamic variables, or rich cards. Use {{variable}} syntax for interpolation.

{
  "message": "Hello, {{customer.name}}! How can I help you today?",
  "type": "text",
  "attachments": [
    {
      "type": "image",
      "url": "https://example.com/images/greeting.png",
      "alt": "Welcome image"
    }
  ]
}

Use adaptive cards for complex layouts. Define them in JSON and embed them via card.adaptive:

{
  "type": "adaptiveCard",
  "version": "1.5",
  "body": [
    {
      "type": "TextBlock",
      "text": "Your order #{{order.id}} is ready.",
      "weight": "bolder"
    }
  ]
}

When a card is not rendered, check the adaptiveCard.schemaVersion field: missing or incorrect versions cause silent rendering failures.

Branch on Conditions and User Input

Use when clauses to route conversations. Conditions are evaluated at runtime using the context object.

{
  "condition": "context.intent === 'support' && context.priority === 'high'",
  "next": "escalate_to_agent"
}

Failures commonly arise from case sensitivity: context.intent vs. context.Intent. Always use toLowerCase() in condition logic.

Use input.text to capture user responses. Configure it with prompt, validation, and timeout.

{
  "input": {
    "type": "text",
    "prompt": "Please describe your issue in detail.",
    "validation": {
      "minLength": 10,
      "regex": "^.{10,}$"
    },
    "timeout": 60000
  }
}

If timeout is not set, the bot waits indefinitely, but the user expects a response. A common error is input.timeout.expired with no fallback.

Integrating with External Systems

Connect the bot to APIs, databases, and services using connector nodes. Each connector has a configuration, operation, and mapping.

Call REST APIs

Use api.call with a pre-defined endpoint. Configure the request body and headers.

{
  "operation": "get",
  "url": "https://api.example.com/v1/customers/{{customer.id}}",
  "headers": {
    "Authorization": "Bearer {{auth.token}}",
    "X-Request-ID": "{{request.id}}"
  },
  "body": {
    "include": ["orders", "preferences"]
  }
}

The body is evaluated in the context before the request is sent. If {{customer.id}} is null, the API returns a 404 but the bot continues without error detection.

Use api.map to transform the response into the bot’s context.

{
  "map": {
    "customer.name": "response.data.name",
    "customer.email": "response.data.email",
    "customer.orders.count": "response.data.orders.length"
  }
}

If the response JSON is deeply nested and a key is missing, the bot silently fails. Check response.data.orders for undefined instead of null. Always verify response.data exists.

Query Databases

Use db.query with a SQL-like syntax.

{
  "query": "SELECT * FROM orders WHERE customer_id = {{customer.id}} AND status = 'pending'",
  "params": {
    "customer.id": "context.customer.id"
  }
}

The params object maps variables to SQL placeholders. Use @param syntax for named parameters.

If the database returns no rows, the bot assumes orders is [], but if the query fails, the error is logged in the db.error field. A common oversight: forgetting to handle db.error in the flow.

Managing State and Context

State is managed through the context object, which persists across turns. Use context.set and context.get.

{
  "action": "set",
  "key": "session.lastInteraction",
  "value": "now"
}

Use context.merge to combine multiple states.

{
  "action": "merge",
  "from": "context.customerData",
  "into": "context"
}

When merging, keys in from overwrite into. If context.customerData has a nested object, context.merge does a shallow merge. Use context.merge.deep for full recursive merging.

Handle Session Lifetime and Persistence

Define session timeout with session.timeout in seconds. After session.timeout, the bot resets context and triggers session.expired.

Use session.save to persist context to storage.

{
  "action": "save",
  "storage": "redis",
  "key": "session:{{session.id}}",
  "ttl": 86400
}

If session.save fails, the bot logs session.save.failed but continues without rollback.

A silent failure: session.id is not set. The bot creates sessions but cannot retrieve them. Use session.id as a key in Redis, but if session.id is null, the session is lost.

Handling User Input and Natural Language

Use NLU (Natural Language Understanding) to extract intents and entities. Configure nlu.extract with a model.

{
  "nlu": {
    "model": "customer-support-v2",
    "threshold": 0.7,
    "extract": [
      "intent",
      "entities",
      "confidence"
    ]
  }
}

The model returns intent.name, entities.location, entities.date, and confidence.score.

Failures occur when confidence.score is not above threshold. Use nlu.fallback to handle low-confidence inputs.

{
  "nlu.fallback": "ask_for_clarification",
  "prompt": "I didn’t understand. Could you rephrase?"
}

If nlu.fallback is not configured, the bot replies with a default message but does not capture user input.

Train and Tune NLU Models

Use nlu.train with a dataset of utterances and labels.

[
  {
    "text": "I need help with my order",
    "intent": "support",
    "entities": {
      "order": "12345"
    }
  },
  {
    "text": "Where is my package?",
    "intent": "track",
    "entities": {
      "trackingNumber": "TRK123456"
    }
  }
]

The model trains on the dataset, but if an utterance has a typo, the model may misclassify it. Use nlu.test to validate.

{
  "nlu.test": [
    "My order is late",
    "Can you track my shipment?"
  ]
}

The nlu.test response includes predicted.intent, predicted.entities, and accuracy.

A common error: nlu.model.not.found when the model name is misspelled or not deployed.

Error Handling and Diagnostics

Use try and catch blocks to manage errors in flows.

{
  "try": [
    "api.call",
    "db.query",
    "nlu.extract"
  ],
  "catch": {
    "error": "api.call.failed",
    "action": "send.message",
    "message": "Sorry, we couldn’t reach our systems. Please try again."
  }
}

If api.call.failed, the bot logs error.api.call.failed and continues. Use error.details to inspect the raw error.

Common error codes:

Use error.log to capture full stack traces.

{
  "action": "log",
  "level": "error",
  "message": "Failed to process order {{order.id}}",
  "details": "{{error.stack}}"
}

The details field is critical: it includes the full context and the error object. A production failure: error.api.call.failed with details showing response.status === 500, but the bot didn’t log response.body.

Testing and Deployment

Test the bot using a test runner that simulates user interactions.

{
  "test": [
    {
      "input": "I want to track my order",
      "expect": {
        "output": "What is your order number?",
        "context": {
          "intent": "track"
        }
      }
    },
    {
      "input": "12345",
      "expect": {
        "output": "Your order 12345 is on the way.",
        "context": {
          "order.id": "12345"
        }
      }
    }
  ]
}

Run tests with test.run and test.report to generate results.

Deploy the bot using a versioned pipeline.

{
  "deploy": {
    "source": "branch:main",
    "target": "environment:staging",
    "version": "1.2.0",
    "changelog": "Added support for returns."
  }
}

The pipeline builds, tests, and deploys the bot. Use deploy.rollout to gradually release to users.

{
  "deploy.rollout": {
    "strategy": "canary",
    "percentage": 25,
    "delay": 300000
  }
}

If canary rollout fails, the bot logs deploy.rollout.failed and rolls back to the previous version.

Monitor and Debug

Use debug.mode to enable real-time tracing.

{
  "debug": {
    "enabled": true,
    "logLevel": "verbose",
    "trace": [
      "nlu.extract",
      "api.call",
      "db.query"
    ]
  }
}

Each step logs debug.step.started, debug.step.completed, and debug.step.error.

A subtle failure: debug.step.completed without debug.step.started. The logs show a step that took 10ms but no start time.

Use debug.context to export the full context at any point.

{
  "action": "debug.context",
  "name": "before.order.processing"
}

This captures context as a JSON blob. Use it to replay the bot’s state in a test environment.

Advanced Patterns

Multi-turn Conversations with Memory

Use memory to persist user preferences across sessions.

{
  "memory": {
    "key": "user:{{user.id}}",
    "ttl": 2592000,
    "persist": [
      "preferredLanguage",
      "lastOrderDate"
    ]
  }
}

If memory.persist is not set, the bot reads from memory but does not update it.

Parallel Flows and Sub-bots

Use flow.parallel to run multiple flows simultaneously.

{
  "flow.parallel": [
    {
      "name": "send.welcome.email",
      "steps": [
        "send.email",
        "update.user.status"
      ]
    },
    {
      "name": "calculate.discount",
      "steps": [
        "get.customer.data",
        "apply.discount.rules"
      ]
    }
  ]
}

Each flow runs independently. Use flow.wait to synchronize.

{
  "flow.wait": {
    "for": [
      "send.welcome.email",
      "calculate.discount"
    ],
    "timeout": 120000
  }
}

If one flow times out, the other continues. The flow.wait node logs flow.wait.timeout and continues.

Reusable Components and Libraries

Create components for common tasks: component.order.status, component.customer.lookup.

{
  "component": "order.status",
  "inputs": [
    "order.id",
    "customer.id"
  ],
  "outputs": [
    "status",
    "estimated.delivery"
  ],
  "steps": [
    "db.query",
    "api.call",
    "send.message"
  ]
}

Use component.use to invoke.

{
  "component.use": {
    "name": "order.status",
    "inputs": {
      "order.id": "{{order.id}}"
    },
    "outputs": {
      "status": "context.order.status",
      "estimated.delivery": "context.delivery"
    }
  }
}

If component.use is not found, the bot logs component.not.found and falls back to a default component.

A production failure: component.use with incorrect input names. Use component.use with order.id but the component expects orderId. Always validate input names with component.schema.

This page was rewritten on 10 October 2026. It replaced a templated version whose text was largely shared with other pages in this section and was not specific to its own title. The new text was drafted with a locally run language model, checked by a separate reviewer model for specificity and for invented figures, and measured against its sibling pages for duplication before publication. If anything here is wrong, tell us at [email protected] and we will correct it.