πŸ“š routex - Awesome Go Library for Artificial Intelligence

Go Gopher mascot for routex

YAML-driven multi-agent AI runtime for Go with Erlang-style supervision, MCP tool server support, and a CLI

🏷️ Artificial Intelligence
πŸ“‚ Artificial Intelligence
⭐ 0 stars
View on GitHub πŸ”—

Detailed Description of routex

  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•—   β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•—  β–ˆβ–ˆβ•—
  β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•”β•β•β•β•β•β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•
  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—   β•šβ–ˆβ–ˆβ–ˆβ•”β•
  β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•”β•β•β•   β–ˆβ–ˆβ•”β–ˆβ–ˆβ•—
  β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β• β–ˆβ–ˆβ•—
  β•šβ•β•  β•šβ•β• β•šβ•β•β•β•β•β•  β•šβ•β•β•β•β•β•    β•šβ•β•   β•šβ•β•β•β•β•β•β•β•šβ•β•  β•šβ•β•

  lightweight AI agent runtime for Go

Go Reference Go Report Card codecov GitHub stars License: MIT Mentioned in Awesome Go

A lightweight AI agent runtime for Go.

Routex lets you build, run, and supervise multi-agent AI crews using the primitives Go developers already know β€” goroutines, channels, and interfaces. Define your crew in a YAML file or pure Go code, wire in any LLM provider and tools, and let the runtime handle scheduling, parallelism, retries, memory, and observability.

go install github.com/Ad3bay0c/routex/cmd/routex@latest
routex init my-crew && cd my-crew
cp .env.example .env   # add your API key
routex run agents.yaml

To depend on Routex from your own Go program (not the CLI):

go get github.com/Ad3bay0c/routex@latest

See Using it as a library for imports and examples.


Why Routex?

The AI agent ecosystem is almost entirely Python. Frameworks like LangGraph and CrewAI are powerful but they carry a Python runtime, slow cold starts, and async complexity that does not belong in production Go services.

Routex is built for Go developers who want agentic capabilities without leaving the Go ecosystem.

LangGraphCrewAIRoutex
LanguagePythonPythonGo
Concurrency modelasyncioasynciogoroutines + channels
Agent supervisionnonenoneErlang-style OTP tree
Binary sizeheavyheavy~10 MB single binary
Cold start~2–5 s~2–5 s~50 ms
Multi-LLM per agentnonoyes
OpenTelemetrypartialpartialfirst-class
Deploy targetPython envPython envany OS, Docker, K8s

Table of Contents


Concepts

Routex borrows ideas from operating systems and applies them to AI agents:

Agent β€” a long-lived goroutine with a brain (LLM), a memory scope, and a set of tools. Agents wait on an Inbox channel, process one task at a time, and send results back through typed channels. They never share state.

Crew β€” a collection of agents that work together on a task. Agents declare depends_on relationships; the scheduler turns these into a DAG and runs independent agents in parallel.

Scheduler β€” performs topological sort (Kahn's algorithm) on the dependency graph. Agents with no dependencies run immediately in parallel. Each subsequent wave starts only when the previous wave completes. Detects cycles at startup, before any agent runs.

Supervisor β€” watches agents via a dedicated notify channel. When an agent fails, the scheduler asks the supervisor what to do. The supervisor checks the agent's restart budget and returns a decision: retry or give up. This design means the scheduler never advances a wave past a failed agent β€” it blocks until the supervisor resolves the failure.

Tool β€” anything an agent can call: web search, file read/write, HTTP request, translation, image generation. Implement one interface, register once, available to any agent.

Memory β€” each agent has a namespaced key-value and message history store. In-memory by default; Redis for persistence across runs.

Runtime β€” the orchestrator. Builds the agent graph, wires in the LLM adapters, starts the supervisor, hands tasks to the scheduler, and collects results.


Quickstart

Using the CLI

The fastest path β€” no Go code required.

# Install
go install github.com/Ad3bay0c/routex/cmd/routex@latest

# Scaffold a new project
routex init my-research-crew
cd my-research-crew

# Set up environment
cp .env.example .env
# Edit .env β€” add your ANTHROPIC_API_KEY

# Edit the generated agents.yaml file

# Validate the config
routex validate agents.yaml

# See the execution plan before running
routex run agents.yaml --dry-run

# Run it
routex run agents.yaml

Override the task inline without editing YAML:

routex run agents.yaml --task "Compare the latest releases of Go and Rust"

Use a different env file (e.g. production secrets injected by your platform):

routex run agents.yaml --env-file .env.staging --output ./reports/result.md

Using it as a library

Import Routex into any Go application. Task input can come from an HTTP request, message queue, database β€” anywhere.

Add the module to your project (this is the library dependency β€” unlike go install, which only builds the routex CLI):

go get github.com/Ad3bay0c/routex@latest

Then import the root package and any subpackages you need (see below). If you add imports first, go mod tidy will record the module as well.

GoalCommand
Use Routex from your Go codego get github.com/Ad3bay0c/routex@version
Install the routex CLI onlygo install github.com/Ad3bay0c/routex/cmd/routex@version

Tool imports β€” opt in to what you need

Routex's built-in tools are in separate sub-packages so you only compile what you use. Import tools/all for everything, or pick individual packages for a leaner binary:

// All 11 built-in tools (convenience β€” larger binary)
import _ "github.com/Ad3bay0c/routex/tools/all"

// Or import only what you need (smaller binary, fewer dependencies)
import (
    _ "github.com/Ad3bay0c/routex/tools/file"   // read_file, write_file
    _ "github.com/Ad3bay0c/routex/tools/search" // web_search, brave_search, wikipedia
    _ "github.com/Ad3bay0c/routex/tools/web"    // http_request, read_url, scrape
    // omit tools/ai   β†’ no OpenAI/Anthropic SDK dependency
    // omit tools/comms β†’ no SendGrid/Resend dependency
)

Option 1 β€” YAML-driven (recommended for most use cases):

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/Ad3bay0c/routex"
    _ "github.com/Ad3bay0c/routex/tools/all" // register all built-in tools
)

func main() {
    ctx := context.Background()

    // LoadConfig reads agents.yaml, loads .env via env_file:,
    // resolves all env: references, and validates the agent graph.
    rt, err := routex.LoadConfig("agents.yaml")
    if err != nil {
        log.Fatal(err)
    }

    result, err := rt.StartAndRun(ctx)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Done in %s β€” %d tokens\n", result.Duration, result.TokensUsed)
    rt.Stop()
}

Option 2 β€” override task at runtime:

rt, _ := routex.LoadConfig("agents.yaml")

// Task comes from an HTTP request, not from YAML
rt.SetTask(routex.Task{
    Input: r.FormValue("topic"),
})

// Or pass directly to LoadConfig
rt, _ := routex.LoadConfig("agents.yaml",
    routex.WithTaskInput(r.FormValue("topic")),
    routex.WithEnvFile(".env.prod"),
)

result, _ := rt.StartAndRun(ctx)

Option 3 β€” fully programmatic (no YAML):

import (
    "github.com/Ad3bay0c/routex"
    "github.com/Ad3bay0c/routex/agents"
    "github.com/Ad3bay0c/routex/tools/search"
    "github.com/Ad3bay0c/routex/tools/file"
)

rt, _ := routex.NewRuntime(routex.Config{
    Name: "my-crew",
    LLM: routex.LLMConfig{
        Provider: "anthropic",
        Model:    "claude-sonnet-4-6",
        APIKey:   os.Getenv("ANTHROPIC_API_KEY"),
    },
})

rt.RegisterTool(search.WebSearch())
rt.RegisterTool(file.WriteFileIn("./outputs"))

rt.AddAgent(agents.AgentConfig{
    ID:    "researcher",
    Role:  agents.Researcher,
    Goal:  "Find recent news about Go 1.24",
    Tools: []string{"web_search"},
})

rt.AddAgent(agents.AgentConfig{
    ID:        "writer",
    Role:      agents.Writer,
    Goal:      "Write a summary report and save it",
    Tools:     []string{"write_file"},
    DependsOn: []string{"researcher"},
})

result, _ := rt.StartAndRun(ctx)

YAML reference

runtime:
  name: "my-crew" # identifies this crew in logs and traces
  llm_provider: "anthropic" # anthropic | openai | ollama
  model: "claude-sonnet-4-6" # model name for the chosen provider
  api_key: "env:ANTHROPIC_API_KEY" # env:VAR reads from environment
  log_level: "info" # debug | info | warn | error
  env_file: "." # DEVELOPMENT ONLY β€” load .env next to this file
  # base_url:   "env:CUSTOM_ENDPOINT"  # override LLM API endpoint

task:
  input: "Research the latest Go releases"
  output_file: "env:OUTPUT_FILE" # env: works in any string field
  max_duration: "5m" # Go duration string β€” 30s, 5m, 1h

tools:
  # Built-in tools β€” declare here, auto-registered by the runtime
  - name: "web_search" # DuckDuckGo, free, no key

  - name: "brave_search" # Higher quality, 2,000 free/month
    api_key: "env:BRAVE_API_KEY"
    max_results: 5

  - name: "wikipedia" # Free, no key needed
    extra:
      language: "en"

  - name: "http_request" # Call any REST API
    api_key: "env:MY_API_KEY" # sent as X-Api-Key header
    extra:
      bearer_token: "env:MY_TOKEN" # sent as Authorization: Bearer
      query_api_key: "env:OWM_KEY" # sent as query param (e.g. OpenWeatherMap)
      query_api_key_name: "appid" # the param name (?appid=KEY)
      param_units: "metric" # default query param on every request
      header_Accept: "application/json"

  - name: "write_file"
    base_dir: "./outputs" # agents can only write inside this dir

  - name: "read_file"
    base_dir: "./data"

  - name: "read_url" # Fetch and strip HTML from any URL

  - name: "scrape" # JS-rendered pages via ScrapingBee
    api_key: "env:SCRAPINGBEE_API_KEY"

  - name: "summarise" # LLM-powered text compression
    api_key: "env:ANTHROPIC_API_KEY"

  - name: "translate" # DeepL API, 500k chars/month free
    api_key: "env:DEEPL_API_KEY"

  - name: "generate_image" # DALL-E 3 via OpenAI
    api_key: "env:OPENAI_API_KEY"
    base_dir: "./outputs/images"

  - name: "send_email" # SendGrid or Resend
    api_key: "env:SENDGRID_API_KEY"
    extra:
      provider: "sendgrid" # sendgrid | resend
      from_email: "[email protected]"
      from_name: "Routex Agent"

  - name: "mcp" # MCP server call
    extra:
      server_url: "http://localhost:3000"
      server_name: "github"

  - name: "mcp"
    extra:
      server_url: "http://localhost:3001"
      server_name: "postgres"

agents:
  - id: "researcher" # unique within this crew
    role: "researcher" # planner | researcher | writer | critic | executor
    goal: "Find and summarise recent Go releases"
    tools: ["web_search", "read_url"]
    restart: "one_for_one" # one_for_one | one_for_all | rest_for_one
    max_retries: 2
    timeout: "90s"
    # depends_on: []                   # list of agent IDs that must complete first
    # max_duplicate_tool_calls: 2      # redirect LLM after N identical tool calls (default: 2)
    # max_total_tool_calls: 20         # absolute tool call budget per attempt (default: 20)

    # Per-agent LLM override β€” uses a different model from the runtime default
    # llm:
    #   provider: "openai"
    #   model:    "gpt-4o"
    #   api_key:  "env:OPENAI_API_KEY"

  - id: "writer"
    role: "writer"
    goal: "Write a markdown report. Save it as report.md"
    tools: ["write_file"]
    depends_on: ["researcher"] # starts only after researcher finishes
    restart: "one_for_one"
    max_retries: 2
    timeout: "120s"

memory:
  backend: "inmem" # inmem | redis
  ttl: "1h"
  # redis_url: "env:REDIS_URL"         # required when backend is "redis"

observability:
  tracing: false
  metrics: false
  # jaeger_endpoint: "env:OTEL_EXPORTER_OTLP_ENDPOINT"  # e.g. http://localhost:4318
  # metrics_addr:    ":9090"           # curl localhost:9090/metrics

The env: prefix

Any string value in the YAML can read from the environment:

api_key: "env:ANTHROPIC_API_KEY" # reads os.Getenv("ANTHROPIC_API_KEY")
output_file: "env:OUTPUT_PATH" # works in any string field
model: "env:LLM_MODEL" # even model names

This means your agents.yaml file can be committed to git with zero secrets in it.


Built-in tools

All tools are registered automatically when listed in the tools: section of agents.yaml. No RegisterTool() call needed for built-ins.

Search & Data

ToolKey neededFree tierDescription
web_searchnoneunlimitedDuckDuckGo Instant Answers
brave_searchBRAVE_API_KEY2,000/monthStructured web results with publication age
wikipedianoneunlimitedWikipedia article summaries, 300+ languages
scrapeSCRAPINGBEE_API_KEY1,000 credits/monthJS-rendered page content

Web & HTTP

ToolKey neededDescription
read_urlnoneFetch and strip HTML from any URL
http_requestconfigurableCall any REST API β€” GET, POST, PUT, PATCH, DELETE

http_request supports four authentication patterns:

# 1. Query string key (OpenWeatherMap, Google Maps)
extra:
  query_api_key:      "env:OWM_KEY"
  query_api_key_name: "appid"         # β†’ ?appid=KEY on every request

# 2. Bearer token (GitHub, most REST APIs)
extra:
  bearer_token: "env:GITHUB_TOKEN"   # β†’ Authorization: Bearer TOKEN

# 3. API key header
api_key: "env:MY_KEY"                # β†’ X-Api-Key: KEY

# 4. Custom header
extra:
  header_X-Custom-Header: "value"    # β†’ X-Custom-Header: value
  header_API-KEY: "value"    # β†’ API-KEY: value

Default query params (sent on every request):

extra:
  param_units: "metric" # β†’ ?units=metric on every request
  param_lang: "en"

File

ToolConfigDescription
write_filebase_dirWrite files β€” sandboxed to base_dir
read_filebase_dirRead files β€” sandboxed to base_dir

Path traversal (../) is blocked in both tools. Agents can only read or write inside the configured base_dir.

AI & Generation

ToolKey neededDescription
summariseANTHROPIC_API_KEYLLM-powered text compression (paragraph, bullets, one-line)
translateDEEPL_API_KEYDeepL translation, 30+ languages, 500k chars/month free
generate_imageOPENAI_API_KEYDALL-E 3 image generation, saves to disk

Communication

ToolKey neededProvidersDescription
send_emailSENDGRID_API_KEY or RESEND_API_KEYSendGrid, ResendSend emails with plain text or HTML

Swap providers with one line β€” no code changes:

- name: "send_email"
  api_key: "env:RESEND_API_KEY"
  extra:
    provider: "resend" # was "sendgrid"
    from_email: "[email protected]"

LLM providers

Set the provider at the runtime level β€” all agents inherit it by default:

runtime:
  llm_provider: "anthropic" # or "openai" or "ollama"
  model: "claude-sonnet-4-6"
  api_key: "env:ANTHROPIC_API_KEY"
ProviderModelsNotes
anthropicclaude-sonnet-4-6, claude-haiku-4-5-20251001, claude-opus-4-6Default
openaigpt-4o, gpt-4o-mini, o1, o3-miniAlso works with OpenAI-compatible APIs
ollamallama3, mistral, phi3, any installed modelLocal inference, no API key needed

Switch providers by changing two lines in YAML β€” zero code changes.


Multi-LLM crews

Each agent can use a different LLM provider and model from the runtime default. This lets you assign the right model to each task β€” use a fast, cheap model for mechanical work and a powerful model only where it matters.

runtime:
  llm_provider: "anthropic"
  model: "claude-haiku-4-5-20251001" # default β€” fast and cheap
  api_key: "env:ANTHROPIC_API_KEY"

agents:
  # Researcher inherits Haiku β€” data fetching doesn't need a powerful model
  - id: "researcher"
    role: "researcher"
    goal: "Fetch weather data from the OpenWeatherMap API"
    tools: ["http_request"]

  # Comparator uses GPT-4o β€” synthesis benefits from a larger model
  - id: "comparator"
    role: "writer"
    goal: "Compare the weather data and write a detailed analysis"
    tools: []
    depends_on: ["researcher"]
    llm:
      provider: "openai"
      model: "gpt-4o"
      api_key: "env:OPENAI_API_KEY"

  # Fact-checker uses Anthropic Sonnet β€” higher quality critique
  - id: "fact_checker"
    role: "critic"
    goal: "Verify the analysis and write the final report"
    tools: ["write_file"]
    depends_on: ["comparator"]
    llm:
      provider: "anthropic"
      model: "claude-sonnet-4-6"
      api_key: "env:ANTHROPIC_API_KEY"

Memory backends

Agents share a namespaced memory store. Each agent's history and outputs are stored under its own key β€” agents cannot accidentally read each other's working memory.

# In-memory (default) β€” no setup, resets between runs
memory:
  backend: "inmem"
  ttl:     "1h"

# Redis β€” persists across runs, shared between instances
memory:
  backend:   "redis"
  ttl:       "24h"
  redis_url: "env:REDIS_URL"

Start Redis locally:

docker run -p 6379:6379 redis:alpine
export REDIS_URL=redis://localhost:6379

MCP tool servers

Routex connects to any MCP (Model Context Protocol) server at startup and registers all tools it exposes β€” making them available to agents exactly like built-in tools. Any MCP-compatible server works: the official GitHub MCP server, the Postgres MCP server, a custom server, or any server from the MCP registry.

tools:
  - name: "mcp"
    extra:
      server_url: "http://localhost:3000" # required β€” your MCP server URL
      server_name: "github" # optional label for logs

  # Multiple servers supported β€” each is one entry
  - name: "mcp"
    extra:
      server_url: "http://localhost:3001"
      server_name: "postgres"

Tools from the server are discovered automatically via tools/list at startup. Agents use them by name just like built-ins:

agents:
  - id: "researcher"
    role: "researcher"
    goal: "Search GitHub for Go MCP servers"
    tools: ["search_repos", "get_user"] # ← names returned by the MCP server

Authentication β€” most MCP servers require credentials. Pass them as headers using the header_* prefix, which supports the env: resolver:

tools:
  - name: "mcp"
    extra:
      server_url: "http://localhost:3000"
      server_name: "github"
      header_Authorization: "env:GITHUB_TOKEN" # β†’ Authorization: Bearer ghp_xxx
      header_X-Api-Key: "env:MY_API_KEY" # β†’ X-Api-Key: abc123

Name collisions β€” if two servers expose a tool with the same name (e.g. both have search), the second is automatically prefixed with its server_name: postgres.search.

routex tools list only shows built-in tools. MCP tools are discovered at runtime β€” run routex run agents.yaml --dry-run to see all available tools after the server connection is made.


Restart policies

Routex supervision is modelled after Erlang/OTP. Set per agent:

- id: "researcher"
  restart: "one_for_one" # default
PolicyWhen an agent fails...Use when...
one_for_oneRestart only that agentAgents are independent
one_for_allRestart the entire crewAgents share state and a partial crew gives wrong results
rest_for_oneRestart the failed agent and all agents that depend on itPipeline β€” downstream agents need fresh upstream output

The restart budget is controlled at the runtime level:

supervisor.New(agents, policy,
    3,             // max restarts per agent
    time.Minute,   // within this sliding window
)

After maxRestarts failures within the window, the supervisor declares the agent permanently failed and propagates the error to the caller.


Observability

Enable tracing and metrics in agents.yaml:

observability:
  tracing: true
  jaeger_endpoint: "http://localhost:4318" # OTLP HTTP β€” Jaeger v1.35+

  metrics: true
  metrics_addr: ":9090"

Start Jaeger locally:

docker run \
  -p 16686:16686 \
  -p 4318:4318 \
  jaegertracing/all-in-one

open http://localhost:16686

Every run produces a trace tree:

routex.run                        ← entire crew run
  routex.agent [researcher]       ← one agent's execution
    routex.llm.complete           ← single LLM call
    routex.tool.execute [http_request]
    routex.llm.complete           ← follow-up after tool result
  routex.agent [comparator]
    routex.llm.complete

Prometheus metrics at :9090/metrics:

routex_tokens_total{agent_id,provider}
routex_tool_calls_total{tool_name,status}
routex_tool_duration_seconds{tool_name}
routex_agent_duration_seconds{agent_id,role}
routex_run_duration_seconds{runtime_name}
routex_agent_failures_total{agent_id}

All observe methods are nil-safe β€” disabling tracing or metrics costs nothing beyond a nil check.


Writing a custom tool

Any struct that implement the Tool interface can be registered:

type Tool interface {
    Name()    string
    Schema()  tools.Schema
    Execute(ctx context.Context, input json.RawMessage) (json.RawMessage, error)
}

Step 1 β€” implement the interface:

// tools/mytools/db_query.go
package mytools

import (
    "context"
    "database/sql"
    "encoding/json"
    "fmt"

    "github.com/Ad3bay0c/routex/tools"
)

type DBQueryTool struct {
    db *sql.DB
}

func (t *DBQueryTool) Name() string { return "db_query" }

func (t *DBQueryTool) Schema() tools.Schema {
    return tools.Schema{
        Description: "Run a read-only SQL query against the application database. " +
            "Returns results as a JSON array of objects.",
        Parameters: map[string]tools.Parameter{
            "query": {
                Type:        "string",
                Description: "A read-only SQL SELECT query. No INSERT, UPDATE, or DELETE.",
                Required:    true,
            },
        },
    }
}

type dbQueryInput struct {
    Query string `json:"query"`
}

func (t *DBQueryTool) Execute(ctx context.Context, input json.RawMessage) (json.RawMessage, error) {
    var params dbQueryInput
    if err := json.Unmarshal(input, &params); err != nil {
        return nil, fmt.Errorf("db_query: invalid input: %w", err)
    }
    if params.Query == "" {
        return nil, fmt.Errorf("db_query: query is required")
    }

    rows, err := t.db.QueryContext(ctx, params.Query)
    if err != nil {
        return nil, fmt.Errorf("db_query: %w", err)
    }
    defer rows.Close()

    // Convert rows to []map[string]any and marshal to JSON
    results, err := rowsToJSON(rows)
    if err != nil {
        return nil, fmt.Errorf("db_query: %w", err)
    }

    return json.Marshal(results)
}

// compile-time check
var _ tools.Tool = (*DBQueryTool)(nil)

Step 2 β€” register it:

rt, _ := routex.LoadConfig("agents.yaml")
rt.RegisterTool(&mytools.DBQueryTool{db: db})
result, _ := rt.StartAndRun(ctx)

Step 3 β€” declare it in agents.yaml:

tools:
  - name: "db_query" # no api_key or extra needed for custom tools

agents:
  - id: "analyst"
    role: "researcher"
    goal: "Query the orders table for last week's top products"
    tools: ["db_query"]

Custom tools are prioritised over built-ins β€” if you register a tool named web_search, it replaces the built-in.


CLI reference

routex <command> [flags]

Commands:
  run       Run an agent crew
  validate  Validate config without running
  tools     Manage built-in tools
  init      Scaffold a new project
  version   Print version info

routex run

routex run <agents.yaml> [flags]

Flags:
  -e, --env-file   <path>      Load .env file (overrides env_file: in YAML)
  -t, --task       <text>      Override task input
  -o, --output     <path>      Override output file path
  -T, --timeout    <duration>  Override max_duration (e.g. 10m, 30s)
  -l, --log-level  <level>     debug | info | warn | error
      --dry-run                Print execution plan and exit
      --json                   Output result as JSON

Examples:
  routex run agents.yaml
  routex run agents.yaml -t "Latest Go security advisories"
  routex run agents.yaml -e .env.staging -o ./reports/$(date +%Y%m%d).md
  routex run agents.yaml --dry-run
  routex run agents.yaml --json | jq '.agents[] | select(.error != null)'

The --dry-run flag validates the full config and prints the execution plan:

routex dry-run  agents.yaml

  wave 1
    lagos_weather                    ← no deps, runs first (in parallel)
    london_weather                   ← no deps, runs first (in parallel)
  wave 2
    comparator       ← lagos_weather, london_weather  [openai / gpt-4o]
  wave 3
    fact_checker     ← comparator

Config is valid. Run without --dry-run to execute.

routex validate

routex validate <agents.yaml> [flags]

Flags:
  -e, --env-file  <path>   Load env file before validation
      --json               Output result as JSON (exit code 0=valid, 1=invalid)

# CI usage
routex validate agents.yaml --json | jq .

routex tools list

routex tools list [flags]

Flags:
  --json   Machine-readable output

Example output:
  Built-in tools (11)

  NAME                       DESCRIPTION
  ─────────────────────────  ─────────────────────────────────────────────
  brave_search               Search the web using Brave Search for accurat...
  generate_image             Generate an image from a text description usin...
  http_request               Make an HTTP request to any REST API endpoint...
  read_file                  Read the content of a local file...
  read_url                   Fetch and strip HTML from any URL...
  scrape                     Fetch JS-rendered page content via ScrapingBee...
  send_email                 Send an email via SendGrid or Resend...
  summarise                  Compress long text using Claude Haiku...
  translate                  Translate text using DeepL...
  web_search                 Search the web via DuckDuckGo (free, no key)...
  wikipedia                  Fetch a Wikipedia article summary...
  write_file                 Write content to a file safely...

routex init

routex init [dirname]

Scaffolds:
  agents.yaml     β€” starter config (two agents, web_search + write_file)
  .env.example    β€” template for required keys
  .gitignore      β€” ignores .env and output files
  main.go         β€” minimal Go entrypoint

Example:
  routex init weather-crew
  cd weather-crew
  cp .env.example .env && vim .env
  go mod init github.com/yourname/weather-crew
  go mod tidy
  routex run agents.yaml

routex version

routex version [--json]

Output:
  routex 1.0.0
  go     go1.22.0
  os     linux/amd64

Typo correction β€” Routex suggests the closest command when you make a mistake:

$ routex runn agents.yaml
routex: unknown command "runn"

Did you mean this?
        routex run

$ routex run agents.yaml --timout 5m
error: unknown flag: --timout

        Did you mean  --timeout  ?

GitHub Actions

You can run Routex in CI the same way you run it locally: check out the repo (with agents.yaml), install the CLI, set API keys as secrets, then invoke routex run. The repository ships a composite action that runs go install and routex run with the same flags as the CLI.

Requirements: use a Linux runner (ubuntu-latest is typical). Set encrypted secrets on the repository or organization for any keys your crew needs (for example ANTHROPIC_API_KEY or OPENAI_API_KEY). Do not put secrets in with: inputs. Fork PRs usually cannot access upstream secrets, so avoid running full agent jobs on untrusted PRs unless you have a deliberate, reviewed policy.

Option A β€” composite action (uses: …/routex@v…)

Pin the action to a release tag so go install uses the same version as the action (when routex_ref is left empty, install uses github.action_ref).

name: Routex crew
on:
  workflow_dispatch:

jobs:
  run:
    runs-on: ubuntu-latest
    env:
      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
    steps:
      - uses: actions/checkout@v4

      - uses: Ad3bay0c/routex@main # pin a semver tag (e.g. v1.0.0) for reproducible installs
        with:
          config: agents.yaml
          task: Summarise the README in three bullet points.
          output: report.md
          timeout: 15m

      - uses: actions/upload-artifact@v4
        with:
          name: routex-output
          path: report.md

Inputs map to CLI flags: config (required), optional task, env_file, output, timeout, log_level, working_directory, go_version, json_output, dry_run, and optional routex_ref to override the install revision.

Option B β€” go install without the composite action

jobs:
  run:
    runs-on: ubuntu-latest
    env:
      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v5
        with:
          go-version: "1.25"

      - name: Install routex
        run: go install github.com/Ad3bay0c/routex/cmd/routex@latest

      - name: Run
        run: |
          echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
          routex run agents.yaml -t "Your task" -o report.md

Safety and cost

Keep timeouts and YAML max_duration reasonable; use dry_run: true (composite) or --dry-run in CI when you only need validation. Restrict tools and tokens in configs that run in CI (least privilege). LLM calls are paid per runβ€”treat workflows like any other production job hitting external APIs.


Environment variables

VariableUsed byDescription
ANTHROPIC_API_KEYruntime, summariseAnthropic API key
OPENAI_API_KEYopenai provider, generate_imageOpenAI API key
BRAVE_API_KEYbrave_searchBrave Search API key
SCRAPINGBEE_API_KEYscrapeScrapingBee API key
DEEPL_API_KEYtranslateDeepL API key (append :fx for free tier)
SENDGRID_API_KEYsend_emailSendGrid API key
RESEND_API_KEYsend_emailResend API key
OPENWEATHER_API_KEYhttp_requestOpenWeatherMap key (passed via query param)
REDIS_URLmemoryRedis connection URL
OTEL_EXPORTER_OTLP_ENDPOINTobserveOTLP trace endpoint
ROUTEX_METRICS_ADDRobservePrometheus metrics address (default :9090)
ROUTEX_TASKconfigOverrides task.input in YAML
ROUTEX_ENV_FILEconfigOverrides env_file: in YAML (set by CLI --env-file)

Development vs Production:

Use env_file: "." in agents.yaml to load a .env file during development:

runtime:
  env_file: "." # DEVELOPMENT ONLY β€” remove in production

In production, inject secrets through your platform instead:

# Docker
docker run -e ANTHROPIC_API_KEY=sk-ant-... myimage

# Kubernetes
kubectl create secret generic routex-secrets \
  --from-literal=ANTHROPIC_API_KEY=sk-ant-...

Repo layout

routex/
β”œβ”€β”€ runtime.go                  # Runtime β€” Start, Run, Stop, ExecutionPlan
β”œβ”€β”€ config.go                   # LoadConfig, YAML parsing, env: resolution
β”œβ”€β”€ task.go                     # Task and Result public types
β”‚
β”œβ”€β”€ agents/
β”‚   β”œβ”€β”€ agent.go                # Agent goroutine β€” think loop, tool dedup, retry
β”‚   β”œβ”€β”€ agent_config.go         # AgentConfig, RestartPolicy, per-agent LLM
β”‚   β”œβ”€β”€ roles.go                # Planner, Researcher, Writer, Critic, Executor
β”‚   β”œβ”€β”€ memory.go               # Agent memory key helpers
β”‚   └── observe.go              # AgentTracer and AgentMetrics interfaces
β”‚
β”œβ”€β”€ llm/
β”‚   β”œβ”€β”€ adapter.go              # Adapter interface, Request, Response, Config
β”‚   β”œβ”€β”€ anthropic.go            # Anthropic provider (claude-*)
β”‚   β”œβ”€β”€ openai.go               # OpenAI provider + compatible APIs
β”‚   └── ollama.go               # Local Ollama provider
β”‚
β”œβ”€β”€ memory/
β”‚   β”œβ”€β”€ store.go                # MemoryStore interface
β”‚   β”œβ”€β”€ inmem.go                # In-memory backend (default)
β”‚   └── redis.go                # Redis backend
β”‚
β”œβ”€β”€ tools/
β”‚   β”œβ”€β”€ tool.go                 # Tool interface, Registry, Schema, Parameter
β”‚   β”œβ”€β”€ builtin.go              # RegisterBuiltin, Resolve, ListBuiltins
β”‚   β”œβ”€β”€ search/
β”‚   β”‚   β”œβ”€β”€ web_search.go       # DuckDuckGo (free)
β”‚   β”‚   β”œβ”€β”€ brave_search.go     # Brave Search API
β”‚   β”‚   └── wikipedia.go        # Wikipedia REST API
β”‚   β”œβ”€β”€ web/
β”‚   β”‚   β”œβ”€β”€ read_url.go         # HTML fetcher + stripper
β”‚   β”‚   β”œβ”€β”€ scrape.go           # JS-rendered pages via ScrapingBee
β”‚   β”‚   └── http_request.go     # Generic HTTP client
β”‚   β”œβ”€β”€ file/
β”‚   β”‚   β”œβ”€β”€ write_file.go       # Sandboxed file writer
β”‚   β”‚   └── read_file.go        # Sandboxed file reader
β”‚   β”œβ”€β”€ ai/
β”‚   β”‚   β”œβ”€β”€ summarise.go        # LLM text compression
β”‚   β”‚   β”œβ”€β”€ translate.go        # DeepL translation
β”‚   β”‚   └── generate_image.go   # DALL-E 3 image generation
β”‚   └── comms/
β”‚       └── send_email.go       # SendGrid + Resend (two providers, one interface)
β”‚
β”œβ”€β”€ observe/
β”‚   β”œβ”€β”€ tracer.go               # OpenTelemetry spans β€” run, agent, LLM, tool
β”‚   └── metrics.go              # Prometheus counters and histograms
β”‚
β”œβ”€β”€ internal/
β”‚   β”œβ”€β”€ scheduler/
β”‚   β”‚   └── scheduler.go        # Kahn's sort, wave execution, failure cooperation
β”‚   └── supervisor/
β”‚       └── supervisor.go       # Erlang-style restart, FailureReport/Decision protocol
β”‚
β”œβ”€β”€ cmd/routex/
β”‚   β”œβ”€β”€ main.go                 # CLI entrypoint
β”‚   β”œβ”€β”€ cli.go                  # Command dispatcher, flag parser, did-you-mean
β”‚   β”œβ”€β”€ cmd_run.go              # routex run
β”‚   β”œβ”€β”€ cmd_validate.go         # routex validate
β”‚   β”œβ”€β”€ cmd_tools.go            # routex tools list
β”‚   β”œβ”€β”€ cmd_init.go             # routex init
β”‚   β”œβ”€β”€ cmd_version.go          # routex version
β”‚   └── suggest.go              # Levenshtein distance for typo correction
β”‚
└── examples/
    β”œβ”€β”€ yaml-driven/            # Minimal YAML-based example
    β”œβ”€β”€ programmatic/           # Pure Go API example
    β”œβ”€β”€ search-and-data/        # Group 1 tools β€” brave_search, wikipedia, scrape
    β”œβ”€β”€ ai-generation/          # Group 2 tools β€” summarise, translate, generate_image
    β”œβ”€β”€ comms-and-storage/      # Group 3 tools β€” send_email, read_file, http_request
    └── weather-compare/        # Multi-LLM crew β€” parallel agents, fact-checker

Contributing

See CONTRIBUTING.md for how to add tools, LLM providers, memory backends, and more.


License

MIT β€” see LICENSE.

Blog