Skip to main content
Like AOF? Give us a star!
If you find AOF useful, please star us on GitHub. It helps us reach more developers and grow the community.

AgentFlow Routing Guide

This guide explains how AOF routes messages to agents using AgentFlows, and how to configure routing for your bots.

Overview

AgentFlow routing is the system that determines which agent handles which message. Instead of hardcoding a single agent for all messages, you can:

  • Route different message patterns to different agents
  • Restrict agents to specific channels or users
  • Run multiple bots on different platforms from a single daemon
  • Implement multi-tenant deployments for enterprises

How Routing Works

When a message arrives, AOF follows this decision tree:

Message Arrives (Telegram/Slack/Discord/WhatsApp)


┌───────────────────────────────────────┐
│ Step 1: FlowRouter.route_best() │
│ - Load all flows from flows.directory │
│ - Score each flow against message │
│ - Pick highest scoring match │
└───────────────┬───────────────────────┘

┌───────┴───────┐
│ │
▼ ▼
[Match Found] [No Match]
│ │
▼ ▼
┌───────────────┐ ┌─────────────────────┐
│ Execute │ │ Step 2: Parse as │
│ AgentFlow │ │ command (/run, etc) │
└───────────────┘ └──────────┬──────────┘

┌───────┴───────┐
│ │
▼ ▼
[Command] [Not Command]
│ │
▼ ▼
┌────────────┐ ┌──────────────────┐
│ Execute │ │ Step 3: Route to │
│ Command │ │ default_agent │
│ Handler │ │ from config │
└────────────┘ └──────────────────┘

Flow Scoring Algorithm

When multiple flows could match a message, the FlowRouter scores each one:

Filter TypeScoreDescription
Exact channel match+100Message from a configured channel
User whitelist match+80Message from an allowed user
Pattern regex match+60Message text matches a pattern
Platform match+40Correct platform (telegram, slack, etc.)
No filters (catch-all)+10Flow with no restrictions

Highest score wins. If scores tie, first defined flow wins.

Example Scoring

# Flow A: score = 40 (platform only)
trigger:
type: Telegram

# Flow B: score = 40 + 60 = 100 (platform + pattern)
trigger:
type: Telegram
config:
patterns: ["kubectl"]

# Flow C: score = 40 + 100 = 140 (platform + channel)
trigger:
type: Telegram
config:
chat_ids: [-1001234567890]

Message "kubectl get pods" in chat -1001234567890:

  • Flow A: 40 points
  • Flow B: 100 points
  • Flow C: 140 points ← Winner

Directory Structure

your-project/
├── daemon-config.yaml # Main config file
├── agents/ # Agent definitions
│ ├── k8s-ops.yaml # metadata.name: k8s-ops
│ ├── incident-agent.yaml # metadata.name: incident-responder
│ └── dev-assistant.yaml # metadata.name: dev-assistant

└── flows/ # AgentFlow definitions
├── telegram-k8s.yaml # Routes K8s messages
├── telegram-incidents.yaml
└── slack-default.yaml

Configuration Reference

Daemon Config

# daemon-config.yaml
apiVersion: aof.dev/v1
kind: DaemonConfig
metadata:
name: my-bot-server

spec:
# Agent discovery
agents:
directory: "./agents" # Path to agent YAML files
watch: false # Hot-reload on changes (optional)

# AgentFlow routing
flows:
directory: "./flows" # Path to flow YAML files
watch: false # Hot-reload on changes (optional)
enabled: true # Enable flow routing

runtime:
# Fallback when no flow matches
default_agent: "k8s-ops" # Must exist in agents directory

AgentFlow Spec

# flows/my-flow.yaml
apiVersion: aof.dev/v1
kind: AgentFlow
metadata:
name: my-flow-name # Unique identifier
labels: # Optional labels for organization
platform: telegram
environment: production

spec:
# Trigger configuration
trigger:
type: Telegram # Platform: Telegram, Slack, Discord, WhatsApp
config:
bot_token: ${TELEGRAM_BOT_TOKEN}

# FILTERS (all optional - omit for catch-all)
chat_ids: [] # Telegram chat IDs
channels: [] # Slack channel names
users: [] # User IDs/usernames
patterns: [] # Regex patterns to match message text

# Agent(s) to handle matched messages
agents:
- name: k8s-ops # Must match metadata.name in agents/
patterns: [] # Optional: sub-routing within flow
description: "" # Optional: for documentation

# Environment context (passed to agent)
context:
kubeconfig: ${KUBECONFIG}
namespace: default
env:
CUSTOM_VAR: "value"

# Approval workflow (optional)
approval:
enabled: true
allowed_users: ["U12345"]
require_for:
- "kubectl delete"

Common Patterns

Pattern 1: Single Agent for All Messages

The simplest setup - one agent handles everything:

# flows/default.yaml
apiVersion: aof.dev/v1
kind: AgentFlow
metadata:
name: default-flow

spec:
trigger:
type: Telegram
config:
bot_token: ${TELEGRAM_BOT_TOKEN}
# No filters = matches ALL messages

agents:
- name: k8s-ops

Pattern 2: Pattern-Based Routing

Route different types of requests to specialized agents:

# flows/k8s-flow.yaml
apiVersion: aof.dev/v1
kind: AgentFlow
metadata:
name: k8s-flow

spec:
trigger:
type: Telegram
config:
patterns:
- "kubectl"
- "k8s"
- "pod"
- "deploy"
- "namespace"

agents:
- name: k8s-ops
---
# flows/incident-flow.yaml
apiVersion: aof.dev/v1
kind: AgentFlow
metadata:
name: incident-flow

spec:
trigger:
type: Telegram
config:
patterns:
- "incident"
- "outage"
- "alert"
- "page"

agents:
- name: incident-responder
---
# flows/default-flow.yaml (catch-all, lowest priority)
apiVersion: aof.dev/v1
kind: AgentFlow
metadata:
name: default-flow

spec:
trigger:
type: Telegram
# No patterns = catch-all for unmatched messages

agents:
- name: general-assistant

Pattern 3: Channel-Based Routing (Slack)

Route different Slack channels to different agents/clusters:

# flows/prod-k8s.yaml
apiVersion: aof.dev/v1
kind: AgentFlow
metadata:
name: prod-k8s-flow

spec:
trigger:
type: Slack
config:
channels:
- production
- prod-alerts
- sre-oncall

agents:
- name: k8s-ops

context:
kubeconfig: ${KUBECONFIG_PROD}
env:
CLUSTER: "production"
REQUIRE_APPROVAL: "true"
---
# flows/staging-k8s.yaml
apiVersion: aof.dev/v1
kind: AgentFlow
metadata:
name: staging-k8s-flow

spec:
trigger:
type: Slack
config:
channels:
- staging
- dev-test

agents:
- name: k8s-ops

context:
kubeconfig: ${KUBECONFIG_STAGING}
env:
CLUSTER: "staging"
REQUIRE_APPROVAL: "false" # No approval in staging

Pattern 4: User-Based Routing

Restrict certain agents to specific users:

# flows/admin-flow.yaml
apiVersion: aof.dev/v1
kind: AgentFlow
metadata:
name: admin-flow

spec:
trigger:
type: Slack
config:
users:
- U015ADMIN # Slack user IDs
- U016SRELEAD

agents:
- name: admin-agent # Has dangerous tools access

approval:
allowed_users:
- U015ADMIN # Self-approval for admins
---
# flows/developer-flow.yaml (everyone else)
apiVersion: aof.dev/v1
kind: AgentFlow
metadata:
name: developer-flow

spec:
trigger:
type: Slack
# No user filter = matches all users

agents:
- name: dev-assistant # Read-only access

Pattern 5: Multi-Platform Same Agent

Share an agent across platforms:

# flows/telegram-assistant.yaml
apiVersion: aof.dev/v1
kind: AgentFlow
metadata:
name: telegram-assistant

spec:
trigger:
type: Telegram
config:
bot_token: ${TELEGRAM_BOT_TOKEN}

agents:
- name: general-assistant
---
# flows/slack-assistant.yaml
apiVersion: aof.dev/v1
kind: AgentFlow
metadata:
name: slack-assistant

spec:
trigger:
type: Slack
config:
bot_token: ${SLACK_BOT_TOKEN}

agents:
- name: general-assistant # Same agent!
---
# flows/discord-assistant.yaml
apiVersion: aof.dev/v1
kind: AgentFlow
metadata:
name: discord-assistant

spec:
trigger:
type: Discord
config:
bot_token: ${DISCORD_BOT_TOKEN}

agents:
- name: general-assistant # Same agent!

Pattern 6: Combining Filters

Combine multiple filters for precise routing:

# flows/prod-k8s-admin.yaml
apiVersion: aof.dev/v1
kind: AgentFlow
metadata:
name: prod-k8s-admin-flow

spec:
trigger:
type: Slack
config:
# ALL conditions must match (AND logic)
channels:
- production
users:
- U015ADMIN
- U016SRELEAD
patterns:
- "kubectl"
- "helm"

agents:
- name: k8s-admin-agent

context:
kubeconfig: ${KUBECONFIG_PROD}

Agent Configuration

Agents referenced in flows must exist in the agents.directory:

# agents/k8s-ops.yaml
apiVersion: aof.dev/v1alpha1
kind: Agent
metadata:
name: k8s-ops # ← Referenced by flows as "k8s-ops"
labels:
category: infrastructure

spec:
model: google:gemini-2.5-flash # Required: LLM model

tools: # Available tools
- kubectl
- helm
- docker
- shell

system_prompt: | # Agent behavior instructions
You are a Kubernetes expert...

max_tokens: 4096
temperature: 0.3

Important: The metadata.name in the agent file must match the agents[].name in the flow.

Debugging Routing

Check What's Loaded

Start the server with debug logging:

RUST_LOG=debug aofctl serve --config daemon-config.yaml

Look for these log messages:

INFO  Pre-loaded 3 agents from "./agents"
INFO Loaded AgentFlow 'telegram-k8s-flow' for platform 'telegram'
INFO Loaded AgentFlow 'slack-default-flow' for platform 'slack'

Check Routing Decisions

When a message arrives:

INFO  Matched AgentFlow 'telegram-k8s-flow' for message (score: 100, reason: PatternMatch)
INFO Using pre-loaded agent: k8s-ops

Common Issues

IssueCauseFix
"No tool executor, tools will be empty"Agent not pre-loadedCheck agents directory path
"Routing to default agent"No flow matchedAdd a catch-all flow
"Agent not found"metadata.name mismatchEnsure flow references correct agent name
"Using fallback model"Agent not in runtimeVerify agent YAML is valid

Best Practices

1. Always Have a Catch-All Flow

# flows/default.yaml
spec:
trigger:
type: Telegram
# No filters = lowest priority catch-all
agents:
- name: general-assistant

2. Use Specific Patterns Over Broad Ones

# ❌ Too broad - matches everything with "get"
patterns: ["get"]

# ✅ Specific - matches K8s commands
patterns: ["kubectl get", "kubectl describe", "k8s"]

3. Organize Flows by Environment

flows/
├── production/
│ ├── k8s-flow.yaml
│ └── incident-flow.yaml
├── staging/
│ └── dev-flow.yaml
└── shared/
└── default-flow.yaml

4. Document Your Flows

metadata:
name: prod-k8s-flow
labels:
environment: production
team: platform
on-call: sre-team
annotations:
description: "Production K8s operations for SRE team"
owner: "platform-team@company.com"

5. Test Routing Before Production

# Dry-run to see which flow would match
aofctl flow test --message "kubectl get pods" --platform telegram
# Output: Would route to 'telegram-k8s-flow' (score: 100)

Complete Example

Here's a complete multi-agent setup:

my-bot/
├── daemon-config.yaml
├── agents/
│ ├── k8s-ops.yaml
│ ├── incident-responder.yaml
│ └── general-assistant.yaml
└── flows/
├── k8s-flow.yaml
├── incident-flow.yaml
└── default-flow.yaml

daemon-config.yaml:

apiVersion: aof.dev/v1
kind: DaemonConfig
metadata:
name: multi-agent-bot

spec:
platforms:
telegram:
enabled: true
bot_token_env: "TELEGRAM_BOT_TOKEN"

agents:
directory: "./agents"

flows:
directory: "./flows"
enabled: true

runtime:
default_agent: "general-assistant"

Start the bot:

export TELEGRAM_BOT_TOKEN="your-token"
export GOOGLE_API_KEY="your-api-key"
aofctl serve --config daemon-config.yaml