Quick Start¶
Build your first RobotLab application in 5 minutes.
Step 1: Set Up API Keys¶
RobotLab reads configuration from environment variables automatically. Set your API key before running any code:
Or create a config file at ./config/robot_lab.yml:
[!IMPORTANT] Config files are flat — put the keys at the top level. Wrapping them in a
defaults:key (as the gem's own bundleddefaults.ymldoes) makes the whole file silently ignored.
See Configuration for all configuration options.
Step 2: Create a Robot¶
Build a simple assistant robot using keyword arguments:
require "robot_lab"
assistant = RobotLab.build(
name: "assistant",
system_prompt: "You are a helpful AI assistant. You provide clear, accurate, and concise answers."
)
Step 3: Run It¶
Send a message and get a response:
The run method takes a positional string message and returns a RobotResult. Use last_text_content to extract the response text.
Complete Example¶
Here is everything together in one file:
require "robot_lab"
# Build a robot with an inline system prompt
assistant = RobotLab.build(
name: "assistant",
system_prompt: "You are a helpful AI assistant. Be concise and friendly."
)
# Run the robot with a message
result = assistant.run("Hello! What can you help me with?")
# Print the response
puts result.last_text_content
Run it:
Using Templates¶
Instead of inline prompts, you can use template files managed by prompt_manager. Templates are .md files with YAML front matter:
---
description: A helpful assistant
parameters:
company_name: null
---
You are a helpful assistant for <%= company_name %>.
Be concise and friendly in your responses.
Create the robot with a template reference and context:
robot = RobotLab.build(
name: "helper",
template: :helper,
context: { company_name: "Acme Corp" }
)
result = robot.run("What services do you offer?")
puts result.last_text_content
Templates are loaded from the prompts/ directory by default (or app/prompts/ in Rails). You can change this in your config.
Adding a Tool¶
Give your robot custom capabilities by defining a RubyLLM::Tool subclass:
require "robot_lab"
class CurrentTime < RubyLLM::Tool
description "Get the current date and time"
param :timezone,
type: "string",
desc: "Timezone name (e.g., 'UTC', 'US/Eastern')",
required: false
def execute(timezone: "UTC")
Time.now.getlocal(timezone_offset(timezone)).strftime("%Y-%m-%d %H:%M:%S %Z")
rescue => e
Time.now.utc.strftime("%Y-%m-%d %H:%M:%S UTC")
end
private
def timezone_offset(tz)
case tz
when "UTC" then "+00:00"
when "US/Eastern" then "-05:00"
when "US/Pacific" then "-08:00"
else "+00:00"
end
end
end
# Pass tools via the local_tools: parameter
assistant = RobotLab.build(
name: "time_bot",
system_prompt: "You are a helpful assistant. Use the current_time tool when users ask about the time.",
local_tools: [CurrentTime]
)
# tools: :inherit is REQUIRED -- run() sends no tools without it
result = assistant.run("What time is it right now?", tools: :inherit)
puts result.last_text_content
Tools are passed to the robot via the local_tools: keyword argument as an array of RubyLLM::Tool subclasses.
[!WARNING] Attaching a tool is not the same as sending it.
Robot#rundefaults totools: :none, which means "send zero tools this turn". Withouttools: :inheriton the run, the example above builds fine, calls the LLM fine, and the model simply never seesCurrentTime— it will guess or say it cannot check the time. The same applies to MCP: passmcp: :inherit, tools: :inheritto connect MCP servers and expose their tools.Do not try to fix this by passing
tools: :inherittoRobotLab.buildfor a standalone robot like this one — at build time it resolves against the global:noneand yields an allowlist matching nothing, suppressing the tools even when the run asks for them. Put it on theruncall.(The one place build-time
:inheritis correct is a robot in a network, where it opts into the networkconfig:'s list — see Creating Networks.)
Method Chaining¶
Robots support a chaining API for runtime adjustments:
robot = RobotLab.build(name: "writer")
result = robot
.with_instructions("You are a creative fiction writer.")
.with_temperature(0.9)
.with_model("claude-sonnet-4")
.run("Write a haiku about programming.")
puts result.last_text_content
The complete set of chaining methods is with_context, with_headers,
with_instructions, with_model, with_params, with_schema,
with_temperature, with_thinking, with_tool, with_tools, plus RobotLab's
own with_template and with_bus.
[!NOTE] There is no
with_max_tokens,with_top_p,with_top_k,with_stop,with_presence_penalty, orwith_frequency_penalty— those raiseNoMethodError. Set them as constructor keyword arguments (RobotLab.build(..., max_tokens: 2000)) or throughwith_params:
Multi-Robot Network¶
Create a pipeline of robots using RobotLab.create_network. Networks use SimpleFlow::Pipeline under the hood with task definitions and dependency tracking:
require "robot_lab"
# Build specialized robots
analyst = RobotLab.build(
name: "analyst",
system_prompt: <<~PROMPT
You are a text analyst. Analyze the given text and provide a brief
summary of its key themes and sentiment. Be concise -- 2-3 sentences max.
PROMPT
)
writer = RobotLab.build(
name: "writer",
system_prompt: <<~PROMPT
You are a professional copywriter. Based on the analysis you receive,
write a short, engaging summary suitable for a newsletter. Keep it
to one paragraph.
PROMPT
)
# Create a sequential pipeline
network = RobotLab.create_network(name: "content_pipeline") do
task :analyst, analyst, depends_on: :none
task :writer, writer, depends_on: [:analyst]
end
# Run the network
result = network.run(
message: "Ruby 3.4 was released with significant performance improvements..."
)
# The final result is from the last robot in the pipeline
if result.value.is_a?(RobotLab::RobotResult)
puts result.value.last_text_content
end
# Access intermediate results by ROBOT name (see note below)
if result.context[:analyst]
puts "\nAnalysis: #{result.context[:analyst].last_text_content}"
end
[!IMPORTANT]
result.contextis keyed by the robot'sname:, not by the task name. Internally each robot writesresult.with_context(@name.to_sym, robot_result). The lookup above works only because the robot is named"analyst"and its task is named:analyst. If you register a robot under a different task name —task :first_pass, analyst— the result is still atresult.context[:analyst], notresult.context[:first_pass]. Keeping the two names identical, as in this example, is the simplest way to avoid the surprise.
Network Task Dependencies¶
Tasks declare their dependencies to control execution order:
| Dependency | Meaning |
|---|---|
depends_on: :none |
Entry point -- runs first with no dependencies |
depends_on: [:task_name] |
Runs after the named task(s) complete |
depends_on: :optional |
Only runs if explicitly activated by a preceding task |
Tasks with non-overlapping dependencies can execute in parallel automatically.
What's Next?¶
You have built your first RobotLab application. Here is where to go next:
-
Learn all configuration options
-
Deep dive into robot creation
-
Give robots custom capabilities
-
Advanced network patterns