Configuration¶
RobotLab uses a layered configuration system powered by MywayConfig. Configuration is loaded automatically from multiple sources with no block-style configure method required.
How Configuration Works¶
Configuration values are loaded in priority order (lowest to highest):
- Bundled defaults --
lib/robot_lab/config/defaults.yml(shipped with the gem) - Environment-specific overrides --
development,test, orproductionsections in defaults.yml - User config file --
~/.config/robot_lab/robot_lab.yml - Project config file --
./config/robot_lab.yml - Environment variables --
ROBOT_LAB_*prefix - Runtime attributes -- e.g.,
RobotLab.config.logger = ...
Higher-priority sources override lower-priority ones. You only need to set the values you want to change.
[!IMPORTANT] The user config file is
~/.config/robot_lab/**robot_lab**.yml— the filename repeats the application name.~/.config/robot_lab/config.ymlis never read, and RobotLab gives no warning when it is present but ignored.
Accessing Configuration¶
Use RobotLab.config to access the configuration object:
# Access nested values with dot notation
RobotLab.config.ruby_llm.model #=> "claude-sonnet-4"
RobotLab.config.ruby_llm.anthropic_api_key #=> "sk-ant-..."
RobotLab.config.ruby_llm.request_timeout #=> 120
RobotLab.config.template_path #=> nil (auto-detected)
# Check the environment
RobotLab.config.development? #=> true/false
configure block
RobotLab.configure yields the config object for block-style setup — useful for setting runtime-only attributes like the logger. For static settings (API keys, timeouts, model defaults) prefer config files or environment variables.
Environment Variables¶
Environment variables use the ROBOT_LAB_ prefix. Use double underscores (__) for nested values:
# Top-level settings
export ROBOT_LAB_TEMPLATE_PATH=prompts
# Nested ruby_llm settings (note the double underscore)
export ROBOT_LAB_RUBY_LLM__MODEL=claude-sonnet-4
export ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
export ROBOT_LAB_RUBY_LLM__OPENAI_API_KEY=sk-...
export ROBOT_LAB_RUBY_LLM__GEMINI_API_KEY=...
export ROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=180
export ROBOT_LAB_RUBY_LLM__MAX_RETRIES=5
The double underscore convention maps to nested YAML structure:
ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY --> ruby_llm.anthropic_api_key
ROBOT_LAB_RUBY_LLM__MODEL --> ruby_llm.model
ROBOT_LAB_TEMPLATE_PATH --> template_path
[!WARNING] Nested values arrive as Strings. Only top-level keys are type-coerced. With
ROBOT_LAB_RUBY_LLM__REQUEST_TIMEOUT=180set,RobotLab.config.ruby_llm.request_timeoutreturns the String"180", not the Integer180. If a numeric nested setting matters to your code, set it in a config file instead, or coerce it yourself with.to_i/.to_f.
Config Files¶
[!WARNING] A
defaults:wrapper is always ignored in your own files. Thedefaults:key you see inside the gem's bundledlib/robot_lab/config/defaults.ymlapplies only to that bundled file. Wrap your own settings in it and every value silently falls back to the default — no error, no warning.# WRONG — silently ignored in a user or project config file defaults: template_path: prompts # RIGHT template_path: promptsSections named for the current environment (
development:,test:,production:) are a different matter, and the two files disagree:
File Flat keys development:section~/.config/robot_lab/robot_lab.ymlhonoured honoured ./config/robot_lab.yml(no Rails)honoured ignored ./config/robot_lab.yml(under Rails)ignored honoured The user file checks for a section matching the current environment and falls back to the file root, so both forms work. The project file is read by anyway_config, which only treats it as environmental once
Anyway::Settings.current_environmentis set — which is precisely what Rails does (it sets it toRails.env). See Rails Integration.
Project Config¶
Create ./config/robot_lab.yml in your project root. Outside Rails, write the
keys at the top level (under Rails they must be nested under the environment name
instead — see Rails Integration):
ruby_llm:
anthropic_api_key: <%= ENV['ANTHROPIC_API_KEY'] %>
model: claude-sonnet-4
request_timeout: 120
max_retries: 3
log_level: info
template_path: prompts
[!NOTE] ERB works here, and only here. The project config file is read through ERB, so
<%= ENV['ANTHROPIC_API_KEY'] %>is expanded before the YAML is parsed. The user config file described below is not — see the warning there.[!WARNING] No YAML symbols in the project config file. It is parsed with an empty permitted-classes list, so
log_level: :inforaisesPsych::DisallowedClass: Tried to load unspecified class: Symboland your application fails to boot. Write the plain stringlog_level: info. (Symbols are permitted in the bundleddefaults.ymland in the user config file, which is why you will see:debugthere.)
User Config¶
Create ~/.config/robot_lab/robot_lab.yml for personal defaults that apply
across all your projects. Keys go at the top level here too:
[!WARNING] Do not put ERB in the user config file. It is parsed with
YAML.safe_loadand never passed through ERB, soanthropic_api_key: <%= ENV['ANTHROPIC_API_KEY'] %>is stored as the literal nine-character-plus string"<%= ENV['ANTHROPIC_API_KEY'] %>"and sent to the provider as your API key. Put secrets in environment variables (ANTHROPIC_API_KEYorROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY), or in the project config file where ERB is evaluated.[!NOTE] The user config file honours both forms: flat keys at the root, or a top-level section named for the current environment (
development:,test:,production:). The loader looks for the environment section first and falls back to the root. Onlydefaults:is ignored here.
Configuration Reference¶
Core Settings¶
| Key | Default | Description |
|---|---|---|
template_path |
null (auto-detected) |
Directory for prompt templates |
mcp |
:none |
Global MCP server configuration |
tools |
:none |
Global tool allowlist |
sandbox.* |
see below | Skill-script confinement ceiling |
[!WARNING] Reserved / not implemented.
defaults.ymlalso shipsmax_iterations,max_tool_iterations,streaming_enabled, and an entirechat:tree (chat.with_model,chat.with_temperature,chat.with_tools,chat.with_params.*). These keys resolve —RobotLab.config.max_iterationsreturns10— but nothing in the library reads them. Setting them has zero effect. They are placeholders; do not build on them.The real equivalents are:
Dead key Use instead max_tool_iterationsmax_tool_rounds:on a robot orRunConfigstreaming_enabledon_content:callback, or a block passed torunchat.with_temperaturetemperature:on a robot orRunConfigchat.with_params.*top_p:/max_tokens:/ etc. on a robot orRunConfigmax_iterationsno equivalent — networks are bounded by their task graph
RubyLLM Settings (ruby_llm: section)¶
All settings under the ruby_llm: key are applied to RubyLLM.configure automatically on startup.
Provider API Keys¶
| Key | Description |
|---|---|
ruby_llm.anthropic_api_key |
Anthropic Claude API key |
ruby_llm.openai_api_key |
OpenAI API key |
ruby_llm.gemini_api_key |
Google Gemini API key |
ruby_llm.deepseek_api_key |
DeepSeek API key |
ruby_llm.mistral_api_key |
Mistral API key |
ruby_llm.openrouter_api_key |
OpenRouter API key |
ruby_llm.bedrock_api_key |
AWS Bedrock access key |
ruby_llm.bedrock_secret_key |
AWS Bedrock secret key |
ruby_llm.bedrock_region |
AWS Bedrock region |
ruby_llm.xai_api_key |
xAI (Grok) API key |
Model Defaults¶
| Key | Default | Description |
|---|---|---|
ruby_llm.model |
claude-sonnet-4 |
Default model for robots that do not set model: |
ruby_llm.default_model |
null |
RubyLLM default model override |
ruby_llm.default_embedding_model |
null |
Default embedding model |
ruby_llm.default_image_model |
null |
Default image model |
[!NOTE]
defaults.ymlalso carriesruby_llm.provider: :anthropicandruby_llm.assume_model_exists: false, but neither is read. A robot's provider comes from its ownprovider:keyword argument, andassume_model_existsis derived from whether that argument was given. Model ids must be full RubyLLM ids — an unknown id raisesRubyLLM::ModelNotFoundErrorwhen the robot is constructed.
Connection Settings¶
| Key | Default | Description |
|---|---|---|
ruby_llm.request_timeout |
120 |
Request timeout in seconds |
ruby_llm.max_retries |
3 |
Maximum retry attempts |
ruby_llm.retry_interval |
1 |
Seconds between retries |
ruby_llm.retry_backoff_factor |
2 |
Exponential backoff factor |
ruby_llm.http_proxy |
null |
HTTP proxy URL |
Provider Endpoints (self-hosted models)¶
| Key | Description |
|---|---|
ruby_llm.openai_api_base |
Custom OpenAI-compatible endpoint |
ruby_llm.gemini_api_base |
Custom Gemini endpoint |
ruby_llm.ollama_api_base |
Ollama endpoint (e.g., http://localhost:11434) |
ruby_llm.gpustack_api_base |
GPUStack endpoint |
Logging¶
| Key | Default | Description |
|---|---|---|
ruby_llm.log_file |
null |
Path to log file |
ruby_llm.log_level |
:info |
Log level (:debug, :info, :warn, :error) |
ruby_llm.log_stream_debug |
false |
Log streaming debug output |
Chat Configuration (chat: section) — not implemented¶
The chat: tree in defaults.yml (chat.with_temperature,
chat.with_params.top_p, chat.with_params.max_tokens, chat.with_tools, …)
is reserved and has no consumers. Values set there are parsed and then
ignored.
To set LLM parameters globally, pass a RunConfig to each robot, or set them per
robot with constructor keyword arguments:
robot = RobotLab.build(
name: "bot",
system_prompt: "You are helpful.",
temperature: 0.7,
max_tokens: 2000,
top_p: 0.9
)
Skill-Script Sandboxing (sandbox: section)¶
Opt-in confinement for the scripts a skill bundle exposes as tools. Disabled by default — scripts run exactly as before until you turn it on:
| Key | Default | Description |
|---|---|---|
sandbox.enabled |
false |
Turn on OS-level confinement for skill scripts |
sandbox.fs_read |
["."] |
Ceiling: paths any skill script may read (relative to cwd) |
sandbox.fs_write |
[] |
Ceiling: paths any skill script may write |
sandbox.network |
false |
Ceiling: whether any skill script may use the network |
sandbox.timeout |
60 |
Ceiling: max seconds a skill script may run |
These are a ceiling, not a grant — the actual permissions a script runs with are the intersection of this ceiling and what the individual SKILL.md declares for itself. See Skill Scripts and Sandboxing for how the two combine and which platforms actually enforce confinement.
Runtime-Only Attributes¶
Some attributes can only be set at runtime, not through config files. Use direct assignment on RobotLab.config or the RobotLab.configure block:
# Direct assignment
RobotLab.config.logger = Logger.new(nil) # silence logging
RobotLab.config.logger = Logger.new("robot.log") # log to file
# Block-style configure (equivalent, useful when setting multiple values)
RobotLab.configure do |c|
c.logger = Logger.new(File::NULL)
end
RobotLab.configure yields the same Config object returned by RobotLab.config.
Reloading Configuration¶
To reload configuration from all sources:
This clears the cached config and reloads from all sources on next access.
Environment-Specific Configuration¶
The defaults.yml shipped with RobotLab includes environment-specific overrides.
This is what the gem actually ships:
The current environment is determined automatically (via RAILS_ENV, RACK_ENV, or defaults to development).
[!NOTE] Under
test, the effective default model isclaude-3-haiku-20240307— a full dated model id. Short aliases likeclaude-haiku-3-5are not valid RubyLLM model ids and raiseRubyLLM::ModelNotFoundErrorat robot construction.
Rails Integration¶
[!NOTE] Core RobotLab ships no Railtie and no Engine. It performs two bare
defined?(::Rails)checks: the default logger becomesRails.logger, andtemplate_pathresolves toRails.root/app/promptswhen left unset. Generators,RobotLab::Job, and Turbo broadcasting live in the separate robot_lab-rails gem.[!WARNING] Under Rails,
./config/robot_lab.ymlmust be environment-sectioned — a flat file is ignored. Rails' anyway_config integration setsAnyway::Settings.current_environmenttoRails.env, which switches the project config loader into environmental mode. Keys then have to live underdevelopment:/test:/production:; anything written at the root of the file is dropped. Outside Rails the rule is exactly inverted — flat keys are read and an environment section is ignored.
development:
ruby_llm:
anthropic_api_key: <%= Rails.application.credentials.anthropic_api_key %>
model: claude-sonnet-4
request_timeout: 180
max_retries: 5
template_path: null # auto-detects app/prompts in Rails
production:
ruby_llm:
anthropic_api_key: <%= Rails.application.credentials.anthropic_api_key %>
model: claude-sonnet-4
request_timeout: 180
max_retries: 5
The same file outside Rails would instead be written flat:
ruby_llm:
model: claude-sonnet-4
request_timeout: 180
template_path: prompts
You can also use Rails credentials:
./config/robot_lab.yml is evaluated through ERB, so credentials can be
referenced inline as shown above. Note that this only works in the project
config file — the ~/.config/robot_lab/robot_lab.yml user file is not run
through ERB.
RunConfig: Shared Operational Defaults¶
RunConfig is a configuration object that lets you express operational defaults for LLM settings, tools, callbacks, and infrastructure. Unlike RobotLab.config (which is global and static), RunConfig flows through the hierarchy and can be customized at each level:
RobotLab.config (global) -> Network RunConfig -> Robot RunConfig -> Template front matter -> Task RunConfig -> Runtime
Creating a RunConfig¶
# Keyword construction
config = RobotLab::RunConfig.new(model: "claude-sonnet-4", temperature: 0.7)
# Block DSL
config = RobotLab::RunConfig.new do |c|
c.model "claude-sonnet-4"
c.temperature 0.7
c.max_tokens 2000
end
# Chaining
config = RobotLab::RunConfig.new
.model("claude-sonnet-4")
.temperature(0.7)
Applying RunConfig¶
Pass config: to robots and networks. Explicit constructor kwargs always override the RunConfig:
# Shared config for a team of robots
shared = RobotLab::RunConfig.new(model: "claude-sonnet-4", temperature: 0.5)
# Robot uses shared config
robot = RobotLab.build(
name: "writer",
system_prompt: "You are a creative writer.",
config: shared,
temperature: 0.9 # overrides shared config's 0.5
)
# Network-level config
network = RobotLab.create_network(name: "pipeline", config: shared) do
task :analyzer, analyzer_robot, depends_on: :none
task :writer, writer_robot, depends_on: [:analyzer]
end
[!WARNING] A network-level
config:propagates onlymcpandtools— and only when the member robot opts in by passingmcp: :inherit/tools: :inheriton itsrun. LLM fields (model,temperature,max_tokens, …) and callbacks (on_content,on_tool_call,on_tool_result) are read from each robot's own config at construction time and are never inherited from the network. The only field the network itself consumes ismax_concurrent_robots.If you want a whole team on one model, pass the same
config:to each robot:shared = RobotLab::RunConfig.new(model: "claude-sonnet-4", temperature: 0.5) analyst = RobotLab.build(name: "analyst", system_prompt: "...", config: shared) writer = RobotLab.build(name: "writer", system_prompt: "...", config: shared)A per-task
config:is merged into the network config and is subject to the samemcp/tools-only limitation.
Merging Configs¶
RunConfig supports merge semantics where the more-specific config's values win:
network_config = RobotLab::RunConfig.new(model: "claude-sonnet-4", temperature: 0.5)
robot_config = RobotLab::RunConfig.new(temperature: 0.9)
effective = network_config.merge(robot_config)
effective.model #=> "claude-sonnet-4" (inherited)
effective.temperature #=> 0.9 (overridden)
Available Fields¶
| Category | Fields |
|---|---|
| LLM | model, temperature, top_p, top_k, max_tokens, presence_penalty, frequency_penalty, stop |
| Tools | mcp, tools |
| Callbacks | on_tool_call, on_tool_result, on_content |
| Infrastructure | bus, enable_cache, max_tool_rounds, token_budget, cost_budget, max_tools, ractor_pool_size, max_concurrent_robots, doom_loop_threshold, auto_compact, compact_threshold |
cost_budget mirrors token_budget for cumulative dollar spend — see Budgets. max_tools overrides the default 128-tool ceiling enforced on every turn before tools are handed to the provider — see Tool Capping.
RunConfig vs RobotLab.config¶
RobotLab.config |
RunConfig |
|
|---|---|---|
| Scope | Global (all robots) | Per-network, per-robot, or per-task |
| Source | YAML files, env vars | Code (constructor, block DSL) |
| Mutability | Loaded once, rarely changed | Created per use case, merged |
| Purpose | API keys, timeouts, defaults | Model, temperature, tools per workflow |
Robot-Level Configuration¶
Individual robots can override the global model and other settings:
# Override model for a specific robot
robot = RobotLab.build(
name: "fast_bot",
system_prompt: "You are a quick responder.",
model: "claude-3-haiku-20240307",
temperature: 0.3,
max_tokens: 500
)
# Or use chaining at runtime
robot.with_temperature(0.9).with_params(max_tokens: 2000).run("Tell me a story.")
[!WARNING] There is no
with_max_tokens,with_top_p,with_top_k,with_stop,with_presence_penalty, orwith_frequency_penalty— calling them raisesNoMethodError. The complete chainable set iswith_context,with_headers,with_instructions,with_model,with_params,with_schema,with_temperature,with_thinking,with_tool,with_tools, plus RobotLab's ownwith_templateandwith_bus. For everything else use a constructor keyword argument orwith_params(...).
Hierarchical MCP and Tools¶
MCP servers and tools use a hierarchical configuration: runtime > robot > network > global. Each level can specify:
:inherit-- Use the parent level's configuration:none-- No MCP servers or tools at this level- An explicit array -- A name allowlist (not a local-vs-MCP switch). Entries are
compared as strings against each attached tool's
name, so they must match the form the tool was attached in: a tool attached as a class matches[RefundTool]or%w[RefundTool], while one attached as an instance matches%w[refund]
[!WARNING]
tools:andmcp:both default to:none— including onrun()itself.Robot#runis declaredrun(message = nil, ..., mcp: :none, tools: :none, ...), and an explicit:nonemeans "send zero tools this turn". So a plainrobot.run("...")sends the LLM no tools and connects no MCP servers, even when you passedlocal_tools:ormcp:at build time.The fix goes on the run, not the build:
robot.run("...", tools: :inherit) # send the attached local tools robot.run("...", mcp: :inherit, tools: :inherit) # connect MCP and send its tools
mcp: :inherittriggers the connection;tools: :inheritis additionally required for the MCP tools to actually be handed to the model.[!IMPORTANT] For a standalone robot, do not pass
tools: :inheritat build time. Build-time:inheritresolves against the parent level, and for a standalone robot the parent is the global:none— producing an allowlist of["none"]that matches nothing and suppresses the tools even when the run asks for:inherit. Verified resolution for a standalone robot:
build tools:run tools:tools sent unset :none(default)none unset :inheritthe attached tools ✅ :inherit:inheritnone ❌ :inherit:nonenone :none:inheritthe attached tools ✅ Leave
tools:unset at build time — unless the robot is a member of a network.[!NOTE] Inside a network, build-time
:inheritis the opt-in, not a bug. The parent is resolved at run time as the network'sconfig:, so:inheritis how a robot asks for the network-level allowlist. WithRobotLab::RunConfig.new(tools: %w[RefundTool])on the network and a robot holdinglocal_tools: [RefundTool, InvoiceTool]:
build tools:task tools:tools sent unset omitted ( :none)none unset :inheritrefund,invoice— the network allowlist is not applied:inherit:inheritrefund— the network allowlist is applied ✅:inheritomitted ( :none)none The same reasoning applies to
mcp:.
# Correct: attach tools at build time, request them at run time
robot = RobotLab.build(
name: "calculator",
system_prompt: "You solve math problems.",
local_tools: [Calculator]
)
robot.run("What is 17 * 23?", tools: :inherit)
# Robot with MCP servers attached at build time
robot = RobotLab.build(
name: "agent",
system_prompt: "You are helpful.",
mcp: [{ name: "filesystem", transport: { type: "stdio", command: "mcp-server-filesystem" } }]
)
robot.run("List the files in ./lib", mcp: :inherit, tools: :inherit)
See Runtime Tool Filtering for the full :inherit/:none/array semantics.
Next Steps¶
- Building Robots - Create custom robots
- Creating Networks - Network configuration
- MCP Integration - Configure MCP servers