Skip to content

Bot Architecture

This guide explains how Flexus bots work under the hood. Understanding these concepts is essential for building effective agents.

Core Concepts

Flexus bots are built around two fundamental concepts: Personas and Experts.

Persona: The Bot's Identity

A Persona is a specific, configured instance of a bot. It defines what the bot is and what it’s responsible for.

Each Persona has:

  • A unique name (e.g., “Customer Support Bot”)
  • Configuration (persona_setup) that controls behavior
  • A link to one or more Experts
  • Its own kanban board for task management

Expert: The Bot's Brain

An Expert provides the core logic and capabilities. It defines how a bot thinks and what it can do.

Each Expert has:

  • A System Prompt — the master instruction set
  • A set of allowed Tools
  • Optional Lark Kernel for execution control
  • Model configuration (which LLM to use)

Why This Separation?

The Persona/Expert split enables powerful patterns:

  1. Reusability — One Expert can power multiple Personas with different configurations
  2. Specialization — A Persona can have multiple Experts for different tasks
  3. Isolation — Each Persona has its own state, kanban board, and budget

Example: You have one “Jira Expert” that knows how to interact with Jira. You can create multiple Personas:

  • “Bug Reporter” — Uses Jira Expert to create bug reports
  • “Sprint Summary” — Uses Jira Expert to generate weekly summaries
  • “Backlog Groomer” — Uses Jira Expert to prioritize tickets

How Bots Execute

┌─────────────────────────────────────────────────────────────┐
│ FLEXUS BACKEND │
│ │
│ ┌─────────┐ ┌───────────┐ ┌─────────┐ │
│ │Scheduler│───►│ Advancer │◄───│ Pubsub │ │
│ └────┬────┘ └─────┬─────┘ └────┬────┘ │
│ │ │ │ │
│ │ ┌─────▼─────┐ │ │
│ │ │ LLM │ │ │
│ │ │ (LiteLLM)│ │ │
│ │ └─────┬─────┘ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ PostgreSQL │ │
│ │ (threads, messages, tasks, personas, experts) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ GraphQL Subscription
┌─────────────────────────────────────────────────────────────┐
│ BOT POD │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │RobotContext │ │Tool Handlers│ │ Integrations│ │
│ │ (rcx) │───►│ @on_tool_* │───►│ Slack, etc │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘

Execution Flow

  1. Event Trigger — A user message, scheduled task, or external event starts the process
  2. Advancer Service — Runs on backend, handles LLM calls and message flow
  3. Lark Kernel — Executes before/after LLM to control behavior
  4. Tool Calls — LLM decides to call tools, routed to bot or cloudtool services
  5. Bot Handler — Your code executes the tool and returns result
  6. Loop — Advancer continues until no more tool calls
Key Insight

LLM calls happen on the backend (Advancer service), not in your bot code. Your bot only receives tool calls and posts results. This keeps bots simple and focused.

Tools: CloudTools vs Inprocess

Flexus has two types of tools:

CloudTools (Backend Services)

These run as backend services, shared across all bots:

ToolPurpose
flexus_bot_kanbanKanban board operations
webWeb search and fetch
flexus_vector_searchVector similarity search
flexus_hand_over_taskDelegate to other bots (A2A)
flexus_colleaguesList available bots

You don’t need to implement these — they’re built-in.

Inprocess Tools (Your Code)

Tools defined in your bot that run in your bot’s process:

from flexus_client_kit import ckit_cloudtool
# Define the tool
MY_TOOL = ckit_cloudtool.CloudTool(
strict=True,
name="my_custom_tool",
description="Does something specific",
parameters={
"type": "object",
"properties": {
"input": {"type": "string", "description": "The input"},
},
"required": ["input"],
"additionalProperties": False,
},
)
# Handler in your main loop
@rcx.on_tool_call(MY_TOOL.name)
async def handle_my_tool(toolcall, args):
result = do_something(args["input"])
return f"Result: {result}"
Important

Every tool in your TOOLS list must have a corresponding @rcx.on_tool_call handler. Missing handlers cause tool calls to be ignored.

Kanban Board

Each Persona has a kanban board for task management:

┌─────────┐ ┌─────────┐ ┌────────────┐ ┌─────────┐
│ INBOX │──►│ TODO │──►│ IN_PROGRESS│──►│ DONE │
└─────────┘ └─────────┘ └────────────┘ └─────────┘
│ │ │
│ │ │
Tasks Bot Scheduler
arrive sorts assigns
here these tasks

How It Works

  1. Inbox — External events (messages, webhooks) create tasks here
  2. Todo — Bot runs on schedule, prioritizes inbox into todo
  3. In Progress — Scheduler assigns tasks from todo, starts conversations
  4. Done — Bot calls flexus_bot_kanban to mark complete

Schedule Types

Bots define schedules in their install script:

TypeWhen It RunsWhat Happens
SCHED_TASK_SORTInbox has itemsBot sorts inbox to todo
SCHED_TODOTodo has itemsAssigns task, starts chat
SCHED_ANYTime-based onlyRuns sched_first_question
SCHED_CREATE_TASKTime-based onlyCreates and assigns task

Subchats

For complex tasks, bots can spawn subchats — isolated conversations that run in parallel:

@rcx.on_tool_call("complex_analysis")
async def handle(toolcall, args):
# Create 3 parallel subchats
subchats = await ckit_ask_model.bot_subchat_create_multiple(
client=fclient,
who_is_asking="parallel_analysis",
persona_id=rcx.persona.persona_id,
first_question=["Analyze aspect 1", "Analyze aspect 2", "Analyze aspect 3"],
first_calls=["null", "null", "null"],
title=["Analysis 1", "Analysis 2", "Analysis 3"],
fcall_id=toolcall.fcall_id,
fexp_name="analyst", # Use specific expert
)
raise ckit_cloudtool.WaitForSubchats(subchats)

Subchats:

  • Run in parallel for speed
  • Have their own context (don’t pollute main conversation)
  • Can use different Experts with specialized tools
  • Must complete in 10 minutes (timeout)
  • Return results to the parent tool call

Lark Kernels

Lark kernels are Starlark scripts that run on backend before/after each LLM call:

# Example Lark kernel
msg = messages[-1]
# Check spending
if coins > budget * 0.8:
post_cd_instruction = "Budget running low. Wrap up soon."
# Detect completion marker
if "TASK_COMPLETE" in str(msg.get("content", "")):
subchat_result = msg["content"]
# Kill unwanted tool calls
if msg.get("tool_calls") and "dangerous_tool" in str(msg["tool_calls"]):
kill_tools = True
error = "That tool is not allowed in this context"

Kernel Variables

DirectionVariablePurpose
InputmessagesConversation history
InputcoinsTokens spent
InputbudgetToken budget
OutputerrorStop with error
Outputkill_toolsCancel tool calls
Outputpost_cd_instructionInject instruction
Outputsubchat_resultReturn subchat result

File Structure

A complete bot consists of these files:

mybot/
__init__.py
mybot_bot.py # Runtime: tools, handlers, main loop
mybot_prompts.py # System prompts for each expert
mybot_install.py # Marketplace registration
mybot-1024x1536.webp # Big marketplace picture
mybot-256x256.webp # Avatar
forms/ # Optional: custom HTML forms
report.html
FileChange Requires
_bot.pyAuto-restart
_prompts.pyReinstall
_install.pyReinstall
ImagesReinstall

Installation Flow

Bots are installed in two stages:

Stage 1: Publish to Marketplace
mybot_install.py --ws=workspace_id
Creates in DB:
- flexus_marketplace record
- flexus_expert records
Returns: marketable_name, marketable_version
(Bot is published but not running)
Stage 2: Hire (Create Persona)
UI: Marketplace → "Hire" button
OR
Code: bot_install_from_marketplace(...)
Creates in DB:
- flexus_persona record
- flexus_persona_schedule records
Returns: persona_id
(Bot can now run)

Next Steps

Now that you understand the architecture: