Tools
Tools let agents interact with the outside world: reading files, making HTTP requests, connecting to MCP servers, calling APIs, or running custom Python functions. They are configured in the spec.tools list, keyed on the type field.
Tool Types
| Type | Description |
|---|---|
filesystem | Read/write files within a sandboxed root directory |
http | Make HTTP requests to a base URL |
mcp | Connect to MCP servers (stdio, SSE, streamable-http) |
custom | Load Python functions from a module |
delegate | Invoke other agents as tool calls |
api | Declarative REST API endpoints defined in YAML |
web_reader | Fetch web pages and convert to markdown |
python | Execute Python code in a subprocess |
datetime | Get current time and parse dates |
sql | Query SQLite databases (read-only) |
git | Run git operations in a subprocess |
shell | Execute shell commands with allowlists |
web_scraper | Scrape web pages and extract structured data |
slack | Send messages via Slack webhooks |
search | Web and news search via DuckDuckGo, SerpAPI, Brave, or Tavily |
email | Search, read, and send emails via IMAP/SMTP |
audio | Fetch YouTube transcripts and transcribe local audio files |
csv_analysis | Inspect, summarize, and query CSV files within a sandboxed root directory |
think | Internal reasoning scratchpad (agent thinks step-by-step without user-visible output) |
script | Inline shell scripts defined in YAML as named, parameterized tools |
calculator | Safe AST-based math expression evaluator with trig, log, and utility functions |
clarify | Agent-initiated human-in-the-loop that asks the user for clarification mid-run (run-scoped) |
image_gen | Generate and edit images via OpenAI DALL-E 3 or Stability AI |
pdf_extract | Extract text and metadata from PDF files |
spawn | Run multiple agent instances in parallel (run-scoped) |
todo | Task management for autonomous agent workflows (run-scoped) |
blackboard | Shared run-scoped key-value board for flow agents to post, read, and claim structured entries (run-scoped) |
| (plugin) | Any other type resolved via the plugin registry |
Quick Example
spec:
tools:
- type: filesystem
root_path: ./src
read_only: true
allowed_extensions: [".py", ".md"]
- type: http
base_url: https://api.example.com
allowed_methods: ["GET", "POST"]
headers:
Authorization: Bearer ${API_TOKEN}
- type: mcp
transport: stdio
command: npx
args: ["-y", "@anthropic/mcp-server-filesystem"]
- type: custom
module: my_tools
config:
db_url: "postgres://..."
- type: api
name: weather
base_url: https://api.weather.com
endpoints:
- name: get_weather
path: "/current/{city}"
parameters:
- name: city
type: string
required: trueTool Permissions
Every built-in tool type has an optional permissions block on its configuration. When present, a PermissionToolset wrapper evaluates glob patterns against call arguments before the tool executes. When absent, no filtering is applied, so existing behavior is preserved.
Fields
| Field | Type | Default | Description |
|---|---|---|---|
default | "allow" | "deny" | "allow" | Policy applied when no rule matches |
allow | list[str] | [] | Patterns that permit a call |
deny | list[str] | [] | Patterns that block a call |
Pattern Format
Two pattern forms are supported:
- Named argument:
arg_name=glob_patternmatches withfnmatchagainst a specific named argument (e.g.command=kubectl *). Since v2026.6.5,arg_namemay be a dotted path (e.g.options.path) to reach a value nested inside a dict argument, and the value is searched leaf-by-leaf so dict, list, and non-string arguments are covered. - Bare glob: a pattern without
=matches against argument values (e.g.*.env). Since v2026.6.5, it recurses into dict and list arguments and stringifies non-string scalars, so a sensitive value nested in a structured argument cannot slip past a deny rule.
Validation rejects empty argument names and empty globs.
Evaluation Order
- Deny rules are checked first. If any deny pattern matches, the call is blocked.
- Allow rules are checked next. If any allow pattern matches, the call is permitted.
- Default policy is applied when no rule matches.
Deny always wins. A call matching both an allow and a deny pattern is blocked.
Examples
Shell, deny by default and allow only safe commands:
tools:
- type: shell
allowed_commands: [kubectl, docker, curl]
permissions:
default: deny
allow:
- command=kubectl get *
- command=kubectl describe *
- command=docker ps *
- command=curl https://*
deny:
- command=rm *Filesystem, allow by default and block sensitive files:
tools:
- type: filesystem
root_path: ./project
permissions:
default: allow
deny:
- "*.env"
- "*credentials*"
- "*.pem"HTTP, block internal and admin endpoints:
tools:
- type: http
base_url: https://api.example.com
permissions:
default: allow
deny:
- "*internal*"
- "*admin*"Denied Response Format
When a call is blocked, the agent receives the message:
Permission denied: {tool_name} -- blocked by rule: {pattern}Raw argument values are never echoed in the denial message to prevent secret leakage.
CSV Analysis
Inspect, summarize, and query CSV files within a sandboxed root directory. Three sub-functions are registered automatically.
tools:
- type: csv_analysis
root_path: ./data
max_rows: 1000
max_file_size_mb: 10.0
delimiter: ","| Field | Type | Default | Description |
|---|---|---|---|
root_path | str | "." | Root directory for CSV file access (path traversal is blocked) |
max_rows | int | 1000 | Maximum rows loaded from the CSV |
max_file_size_mb | float | 10.0 | Maximum CSV file size in MB |
delimiter | str | "," | CSV delimiter character |
Registered functions:
inspect_csv(path): returns column names, types, row count, and a sample of the first few rows.summarize_csv(path, column): returns per-column statistics. Numeric columns: min, max, mean, median, stdev. Categorical columns: unique count and top values.query_csv(path, filter_column, filter_value, columns, limit): filters rows by exact column=value match and returns a markdown table.
Filesystem
Sandboxed file operations within a root directory. Paths cannot escape the root (path traversal is blocked).
tools:
- type: filesystem
root_path: ./src
read_only: true
allowed_extensions: [".py", ".md", ".txt"]| Field | Type | Default | Description |
|---|---|---|---|
root_path | str | "." | Root directory for file operations |
allowed_extensions | list[str] | [] | File extensions to allow (empty = all) |
read_only | bool | true | Only allow read operations |
Registered functions: read_file(path), list_directory(path), and write_file(path, content) (when read_only: false).
HTTP
Makes HTTP requests to a configured base URL.
tools:
- type: http
base_url: https://api.example.com
allowed_methods: ["GET"]
headers:
Authorization: Bearer ${API_TOKEN}| Field | Type | Default | Description |
|---|---|---|---|
base_url | str | (required) | Base URL for requests |
allowed_methods | list[str] | ["GET"] | Allowed HTTP methods |
headers | dict | {} | Headers sent with every request |
Registered function: http_request(method, path, body).
MCP
Connects to MCP (Model Context Protocol) servers, exposing their tools to the agent.
tools:
# Stdio transport (local process)
- type: mcp
transport: stdio
command: npx
args: ["-y", "@anthropic/mcp-server-filesystem"]
# SSE transport (remote server)
- type: mcp
transport: sse
url: http://localhost:3001/sse
# Streamable HTTP transport
- type: mcp
transport: streamable-http
url: http://localhost:3001/mcp
tool_filter: [search, get_document]| Field | Type | Default | Description |
|---|---|---|---|
transport | str | "stdio" | "stdio", "sse", or "streamable-http" |
command | str | null | null | Command for stdio transport |
args | list[str] | [] | Arguments for the stdio command |
url | str | null | null | URL for SSE or streamable-http transport |
tool_filter | list[str] | [] | Only expose these tools (empty = all; mutually exclusive with tool_exclude) |
tool_exclude | list[str] | [] | Exclude these tools (mutually exclusive with tool_filter) |
headers | dict | {} | HTTP headers for SSE/streamable-http transport |
env | dict | {} | Environment variables passed to the stdio subprocess |
cwd | str | null | null | Working directory for the stdio subprocess |
tool_prefix | str | null | null | Prefix added to tool names to avoid collisions |
max_retries | int | 1 | Maximum connection retry attempts |
timeout_seconds | int | null | null | Connection timeout in seconds |
defer | bool | false | Defer server connection until first tool call; serve cached schemas meanwhile. See Deferred Tool Loading |
Custom
Load Python functions from a module and register them as agent tools.
tools:
# Auto-discover all public functions
- type: custom
module: my_tools
# Load a single function
- type: custom
module: my_tools
function: search_db
# With config injection
- type: custom
module: my_tools
config:
api_key: ${MY_API_KEY}| Field | Type | Default | Description |
|---|---|---|---|
module | str | (required) | Python module path (must be importable) |
function | str | null | null | Specific function to load (null = auto-discover all) |
config | dict | {} | Config injected into functions with a tool_config parameter |
Functions that declare a tool_config parameter receive the config dict automatically, and the parameter is hidden from the LLM.
Installed bundles. Since v2026.6.1, a
customtool whose module ships inside an installed role bundle (ahub__*oroci__*directory, or a directory with a bundlemanifest.json) will not import its module code unlessINITRUNNER_ALLOW_TOOL_CODE=1is set in the environment. The flag is an environment variable only, never a role-YAML field, so a bundle cannot grant itself trust. Locally-authored custom tools are unaffected and load normally. See Security for details.
Scaffold a tool module from a natural-language description (since v2026.6.9):
initrunner tool new "look up the current weather for a city"This LLM-scaffolds a type: custom module plus a pytest stub. For a static starter without an LLM, use initrunner new --template tool. The Scaffold and Iterate loop below shows how to test a tool live without restarting.
Complete Custom Tool Walkthrough
Here's a full example with the Python module and the role YAML that uses it.
my_tools.py (every public function becomes an agent tool):
"""Custom tools module for InitRunner.
All public functions are auto-discovered as agent tools. Type annotations and
docstrings are used as tool schemas and descriptions. Functions accepting a
``tool_config`` parameter receive the config dict from role.yaml (hidden from
the LLM).
"""
import hashlib
import json
import uuid
def convert_units(value: float, from_unit: str, to_unit: str) -> str:
"""Convert a numeric value between common measurement units.
Supported conversions: km/mi, kg/lb, c/f, l/gal, m/ft, cm/in.
"""
conversions: dict[tuple[str, str], float | None] = {
("km", "mi"): 0.621371,
("mi", "km"): 1.60934,
("kg", "lb"): 2.20462,
("lb", "kg"): 0.453592,
("c", "f"): None,
("f", "c"): None,
("l", "gal"): 0.264172,
("gal", "l"): 3.78541,
("m", "ft"): 3.28084,
("ft", "m"): 0.3048,
("cm", "in"): 0.393701,
("in", "cm"): 2.54,
}
key = (from_unit.lower(), to_unit.lower())
if key == ("c", "f"):
result = value * 9 / 5 + 32
elif key == ("f", "c"):
result = (value - 32) * 5 / 9
elif key in conversions:
result = value * conversions[key]
else:
return f"Unsupported conversion: {from_unit} -> {to_unit}"
return f"{value} {from_unit} = {result:.4f} {to_unit}"
def generate_uuid() -> str:
"""Generate a random UUID v4 identifier."""
return str(uuid.uuid4())
def format_json(text: str) -> str:
"""Pretty-print a JSON string with 2-space indentation."""
try:
parsed = json.loads(text)
return json.dumps(parsed, indent=2, ensure_ascii=False)
except json.JSONDecodeError as e:
return f"Invalid JSON: {e}"
def word_count(text: str) -> str:
"""Count words, characters, and lines in a text string."""
words = len(text.split())
chars = len(text)
lines = text.count("\n") + 1 if text else 0
return f"Words: {words}, Characters: {chars}, Lines: {lines}"
def hash_text(text: str, algorithm: str = "sha256") -> str:
"""Hash text using the specified algorithm (md5, sha1, sha256, sha512)."""
algo = algorithm.lower()
if algo not in ("md5", "sha1", "sha256", "sha512"):
return f"Unsupported algorithm: {algorithm}. Use md5, sha1, sha256, or sha512."
h = hashlib.new(algo)
h.update(text.encode())
return f"{algo}:{h.hexdigest()}"
def lookup_with_config(query: str, tool_config: dict) -> str:
"""Look up a query using the configured prefix and source.
The tool_config parameter is injected by InitRunner from the role YAML
and is hidden from the LLM.
"""
prefix = tool_config.get("prefix", "DEFAULT")
source = tool_config.get("source", "unknown")
return f"[{prefix}] Result for '{query}' from source '{source}'"custom-tools-demo.yaml (the role that loads it):
apiVersion: initrunner/v1
kind: Agent
metadata:
name: custom-tools-demo
description: Demonstrates custom tool type with auto-discovered Python functions
spec:
role: |
You are a utility assistant with access to custom tools defined in a Python
module. Use these tools to help the user with practical tasks.
Available custom tools:
- convert_units: Convert between common measurement units
- generate_uuid: Generate a random UUID v4 identifier
- format_json: Pretty-print a JSON string
- word_count: Count words, characters, and lines in text
- hash_text: Hash text with md5, sha1, sha256, or sha512
- lookup_with_config: Look up a query using the configured prefix and source
Always use the appropriate tool rather than trying to compute results yourself.
model:
provider: openai
name: gpt-4o-mini
temperature: 0.1
tools:
- type: custom
module: my_tools
config:
prefix: "DEMO"
source: "custom-tools-demo"
- type: datetime
guardrails:
max_tokens_per_run: 20000
max_tool_calls: 15
timeout_seconds: 60Run from the directory containing both files:
cd examples/roles/custom-tools-demo
initrunner run custom-tools-demo.yaml -iExample prompts:
> Convert 72 degrees Fahrenheit to Celsius
> Generate a UUID for me
> Hash "hello world" with sha256
> Look up "test query"Key patterns: Docstrings become tool descriptions. Type annotations become parameter schemas. The
tool_configparameter is injected from the YAMLconfigblock and hidden from the LLM, so the agent never seesprefixorsourceas callable parameters. Omittingfunctionin the YAML auto-discovers all public functions in the module.
Scaffold and Iterate with tool new
Since v2026.6.9, you can scaffold a custom tool from a description instead of writing the module by hand:
initrunner tool new "fetch the current weather for a city"This writes <name>.py and test_<name>.py, prints the generated module, and prints a paste-ready snippet:
tools:
- type: custom
module: <name>Generated functions default to async def, accept an optional tool_config: dict for config and secrets (injected from the role's config: block, hidden from the model), and avoid sandbox-blocked imports (network through httpx or urllib is allowed). The source is AST-validated before it is written and is never imported during scaffolding; on a validation failure the command retries once. Passing --output mytools.py sets the module name and retargets the generated test's import.
To test a tool without restarting, run the developer REPL:
initrunner run role.yaml --dev--dev turns off streaming and the status spinner so a breakpoint() in a tool owns the terminal for pdb. Inside the REPL:
/tool add <module>appends atype: customtool for that module and rebuilds the agent in place, reloading the edited module, so a freshly scaffolded tool is callable on the next turn with the conversation preserved./reloadrebuilds the agent after you editrole.yaml.
Both swap the live agent atomically and carry over templating values, the open memory store, and resume context. They work in any interactive REPL (-i); --dev just makes the loop debugger-friendly. Once the tool works, paste the printed snippet into your role's tools:.
API
Declarative REST API endpoints defined entirely in YAML, with no Python required.
tools:
- type: api
name: github
description: GitHub REST API
base_url: https://api.github.com
headers:
Accept: application/vnd.github.v3+json
auth:
Authorization: "Bearer ${GITHUB_TOKEN}"
endpoints:
- name: get_repo
method: GET
path: "/repos/{owner}/{repo}"
description: Get repository information
parameters:
- name: owner
type: string
required: true
- name: repo
type: string
required: true
response_extract: "$.full_name"
- name: create_issue
method: POST
path: "/repos/{owner}/{repo}/issues"
description: Create a new issue
parameters:
- name: owner
type: string
required: true
- name: repo
type: string
required: true
- name: title
type: string
required: true
- name: body
type: string
required: false
default: ""
body_template:
title: "{title}"
body: "{body}"
response_extract: "$.html_url"| Field | Type | Default | Description |
|---|---|---|---|
name | str | (required) | API group name |
base_url | str | (required) | Base URL for all endpoints |
headers | dict | {} | Headers sent with every request (supports ${VAR}) |
auth | dict | {} | Auth headers merged into headers |
endpoints | list | (required) | Endpoint definitions |
Each endpoint supports name, method, path, description, parameters, headers, body_template, query_params, response_extract, and timeout_seconds.
Scaffold an API tool agent:
initrunner new --template apiDelegate
Invoke other agents as tool calls. Each agent reference generates a delegate_to_{name} tool.
tools:
- type: delegate
agents:
- name: summarizer
role_file: ./roles/summarizer.yaml
description: "Summarizes long text"
- name: researcher
role_file: ./roles/researcher.yaml
description: "Researches topics"
mode: inline
max_depth: 3
timeout_seconds: 120| Field | Type | Default | Description |
|---|---|---|---|
agents | list | (required) | Agent references (name + role_file or url) |
mode | str | "inline" | "inline" (in-process), "mcp" (HTTP), or "a2a" (A2A protocol) |
max_depth | int | 3 | Maximum delegation recursion depth |
timeout_seconds | int | 120 | Timeout per delegation call |
shared_memory | object | null | null | Shared memory config with store_path (str) and max_memories (int, default 1000) |
agents[].headers_env | dict | null | null | Map of header name to env var name (for mcp and a2a modes) |
The a2a mode sends JSON-RPC requests to a remote A2A server and polls for results. Use it to call agents running on other machines or in other frameworks. Each agent reference needs a url instead of a role_file.
Git
Subprocess-based git operations with read-only default.
tools:
- type: git
repo_path: .
read_only: true
timeout_seconds: 30| Field | Type | Default | Description |
|---|---|---|---|
repo_path | str | "." | Path to the git repository |
read_only | bool | true | Only allow read operations |
timeout_seconds | int | 30 | Timeout for each git command |
Read tools: git_status, git_log, git_diff, git_show, git_blame, git_changed_files, git_list_files. Write tools (when read_only: false): git_checkout, git_commit, git_tag.
Shell
Execute shell commands with an allowlist.
tools:
- type: shell
allowed_commands: [kubectl, docker, curl]
require_confirmation: false
timeout_seconds: 30
working_dir: .| Field | Type | Default | Description |
|---|---|---|---|
allowed_commands | list[str] | [] | Allowlist of executable names; empty = all non-blocked commands are permitted |
blocked_commands | list[str] | (built-in denylist) | Commands always blocked regardless of allowed_commands (e.g. rm, sudo) |
require_confirmation | bool | true | Prompt user before each execution |
timeout_seconds | int | 30 | Timeout per command in seconds |
working_dir | str | null | null | Working directory (null = role file's directory) |
max_output_bytes | int | 102400 | Truncate combined stdout+stderr beyond this byte count |
Registered function: run_shell(command). Shell operators (|, &&, ;, redirects) are blocked, so use dedicated tools instead. When allowed_commands is empty, all non-blocked commands are permitted; when non-empty, only listed executables are allowed.
When
security.sandboxis enabled, commands run inside the configured sandbox backend (bubblewrap or Docker) instead of on the host.
Web Reader
Fetch a web page and return its content as markdown. Internal (SSRF) addresses are automatically blocked.
tools:
- type: web_reader
allowed_domains: []
timeout_seconds: 15
max_content_bytes: 512000| Field | Type | Default | Description |
|---|---|---|---|
allowed_domains | list[str] | [] | Only fetch from these domains (empty = allow all) |
blocked_domains | list[str] | [] | Never fetch from these domains (ignored when allowed_domains is set) |
max_content_bytes | int | 512000 | Truncate page content beyond this byte count |
timeout_seconds | int | 15 | HTTP request timeout in seconds |
user_agent | str | (default) | User-Agent header sent with requests |
Registered function: fetch_page(url).
Python
Execute Python code in a subprocess with optional network isolation.
tools:
- type: python
timeout_seconds: 30
network_disabled: true
require_confirmation: true| Field | Type | Default | Description |
|---|---|---|---|
timeout_seconds | int | 30 | Timeout per execution in seconds |
max_output_bytes | int | 102400 | Truncate combined stdout+stderr beyond this byte count |
working_dir | str | null | null | Working directory (null = fresh temp directory per run) |
require_confirmation | bool | true | Prompt user before each execution |
network_disabled | bool | true | Block outbound network access via audit hook |
Registered function: run_python(code).
When
security.sandboxis enabled, code runs inside the configured sandbox backend (bubblewrap or Docker) instead of on the host.
DateTime
Get the current date/time and parse date strings. Requires no API key or external service.
tools:
- type: datetime
default_timezone: UTC| Field | Type | Default | Description |
|---|---|---|---|
default_timezone | str | "UTC" | Default timezone when none is specified in the tool call |
Registered functions: current_time(timezone), parse_date(text, format).
SQL
Query a SQLite database. Read-only by default. ATTACH DATABASE is blocked at the engine level to prevent escaping the configured database.
tools:
- type: sql
database: ./data.db
read_only: true
max_rows: 100| Field | Type | Default | Description |
|---|---|---|---|
database | str | (required) | Path to the SQLite file, or :memory: for an in-memory database |
read_only | bool | true | Only allow SELECT statements |
max_rows | int | 100 | Maximum rows returned per query |
max_result_bytes | int | 102400 | Truncate result output beyond this byte count |
timeout_seconds | int | 10 | SQLite connection timeout in seconds |
Registered function: query_database(sql).
Web Scraper
Fetch a web page, extract its content, and store it in the document store so it becomes searchable via search_documents. Uses the chunking and embedding settings from spec.ingest.
tools:
- type: web_scraper
allowed_domains: []
timeout_seconds: 15| Field | Type | Default | Description |
|---|---|---|---|
allowed_domains | list[str] | [] | Only scrape these domains (empty = allow all) |
blocked_domains | list[str] | [] | Never scrape these domains (ignored when allowed_domains is set) |
max_content_bytes | int | 512000 | Truncate page content beyond this byte count |
timeout_seconds | int | 15 | HTTP request timeout in seconds |
user_agent | str | (default) | User-Agent header sent with requests |
Registered function: scrape_page(url). After scraping, the page is chunked and embedded using the settings from spec.ingest, then stored so search_documents can retrieve it.
Search
Web and news search via pluggable providers. The default provider (DuckDuckGo) requires no API key.
tools:
- type: search
provider: duckduckgo
max_results: 10
safe_search: true
timeout_seconds: 15| Field | Type | Default | Description |
|---|---|---|---|
provider | str | "duckduckgo" | Search backend to use |
api_key | str | null | null | API key (required for paid providers) |
max_results | int | 10 | Maximum results per query |
safe_search | bool | true | Enable safe-search filtering |
timeout_seconds | int | 15 | Timeout for each search request |
Providers
| Provider | API key required | Notes |
|---|---|---|
duckduckgo | No | Free, no account needed |
serpapi | Yes | Google results via SerpAPI |
brave | Yes | Brave Search API |
tavily | Yes | Tavily search API |
Registered functions: web_search(query, num_results), news_search(query, num_results, days_back).
Install the search extra for the DuckDuckGo provider:
pip install initrunner[search]Slack
Send messages to Slack channels via incoming webhooks.
tools:
- type: slack
webhook_url: ${SLACK_WEBHOOK_URL}
default_channel: "#general"
username: "InitRunner Bot"
icon_emoji: ":robot_face:"
timeout_seconds: 30
max_response_bytes: 1024| Field | Type | Default | Description |
|---|---|---|---|
webhook_url | str | (required) | Slack incoming webhook URL |
default_channel | str | null | null | Override the webhook's default channel |
username | str | null | null | Bot username override |
icon_emoji | str | null | null | Bot icon emoji (e.g. :robot_face:) |
timeout_seconds | int | 30 | HTTP request timeout in seconds |
max_response_bytes | int | 1024 | Truncate Slack API response beyond this byte count |
Registered function: send_slack_message(text, channel?, blocks?).
Search, read, and send emails via IMAP/SMTP. Read-only by default, so sending requires explicit opt-in.
tools:
- type: email
imap_host: imap.gmail.com
smtp_host: smtp.gmail.com
imap_port: 993
smtp_port: 587
username: ${EMAIL_USER}
password: ${EMAIL_PASSWORD}
use_ssl: true
default_folder: INBOX
read_only: true
max_results: 20
max_body_chars: 50000
timeout_seconds: 30| Field | Type | Default | Description |
|---|---|---|---|
imap_host | str | (required) | IMAP server hostname |
smtp_host | str | null | null | SMTP server hostname (required for sending) |
imap_port | int | 993 | IMAP port |
smtp_port | int | 587 | SMTP port |
username | str | (required) | Email account username |
password | str | (required) | Email account password (supports ${VAR}) |
use_ssl | bool | true | Use SSL/TLS for connections |
default_folder | str | "INBOX" | Default mailbox folder |
read_only | bool | true | Only allow read operations |
max_results | int | 20 | Maximum emails returned per search |
max_body_chars | int | 50000 | Truncate email bodies beyond this length |
timeout_seconds | int | 30 | Timeout for IMAP/SMTP operations |
Registered functions: search_inbox(query, folder, limit), read_email(message_id, folder), list_folders().
When read_only: false, an additional function is registered: send_email(to, subject, body, reply_to, cc).
Security: The email tool defaults to read-only mode. Use environment variables (
${EMAIL_USER},${EMAIL_PASSWORD}) for credentials. Never hard-code them in YAML.
Audio
Fetch YouTube video transcripts and transcribe local audio/video files.
Requires the audio extra (pip install initrunner[audio]).
tools:
- type: audio
youtube_languages: ["en"]
include_timestamps: false
transcription_model: null # defaults to spec.model
max_audio_mb: 20.0
max_transcript_chars: 50000| Field | Type | Default | Description |
|---|---|---|---|
youtube_languages | list[str] | ["en"] | Preferred caption language codes for YouTube transcripts |
include_timestamps | bool | false | Include timestamps in transcript output |
transcription_model | str | null | null | Multimodal model for local transcription (e.g. openai:gpt-4o-audio-preview); defaults to the agent's model |
max_audio_mb | float | 20.0 | Maximum local file size to send for transcription |
max_transcript_chars | int | 50000 | Truncate transcript output beyond this length |
Registered functions: get_youtube_transcript(url, language), transcribe_audio(file_path).
Supported audio formats: .mp3, .mp4, .m4a, .wav, .ogg, .webm, .mpeg, .flac.
Model requirement:
transcribe_audiopasses audio to the agent's model (ortranscription_modelif set). Use a model that supports audio input such asopenai:gpt-4o-audio-preview. See Multimodal for supported models.
Example: meeting notes agent
spec:
model:
provider: openai
name: gpt-4o-audio-preview
tools:
- type: audio
include_timestamps: true
max_audio_mb: 25.0Think Tool
Gives the agent an accumulated reasoning scratchpad. Each call appends a thought and returns the full numbered chain, which survives context trimming. An optional ring buffer caps token overhead, and periodic self-critique nudges keep reasoning on track.
tools:
- type: think
critique: true
max_thoughts: 30Options
| Field | Type | Default | Description |
|---|---|---|---|
critique | bool | false | Append a self-critique nudge every 5th thought |
max_thoughts | int | 50 | Ring buffer capacity (1–200). Oldest thoughts are evicted when full |
Registered Functions
think(thought: str) -> str: appends a thought and returns the full numbered chain. Withcritique: true, every 5th thought includes a nudge: "You have recorded N thoughts. Before proceeding, critically evaluate your reasoning so far. What assumptions might be wrong? What have you missed?"
When to Use
- Always add
type: thinkfor agents doing multi-step reasoning. - Enable
critique: truefor complex tasks where self-correction matters. - Reduce
max_thoughtsfor agents with tight token budgets.
The think tool works in both single-shot and autonomous mode. In autonomous mode, thoughts persist across iterations through run-scoped state. See Reasoning Primitives for strategies that orchestrate thinking across turns.
Example
# Careful reasoning agent with self-critique
spec:
role: >
You are a careful, methodical assistant. Before answering any question
or taking any action, always use the think tool to reason step-by-step.
model:
provider: openai
name: gpt-5-mini
tools:
- type: think
critique: true
- type: datetimeTodo Tool
Priority-aware task management with dependency resolution. The agent creates structured todo lists, works through items by priority, and auto-completes when all items reach terminal status. Operates on run-scoped state that is fresh per run and never leaks across sessions.
tools:
- type: todo
max_items: 30Options
| Field | Type | Default | Description |
|---|---|---|---|
max_items | int | 30 | Maximum concurrent items (1–100) |
shared | bool | false | Back state with SQLite for sub-agent access |
shared_path | str | "" | SQLite file path (required when shared: true) |
Registered Functions
| Tool | Description |
|---|---|
add_todo(description, priority?, depends_on?) | Create an item. Returns its 8-char ID + the full formatted list |
batch_add_todos(items) | Create multiple items at once. Supports inter-batch dependency refs via index ("0", "1", ...) |
update_todo(id, status?, notes?, priority?) | Update fields on an existing item. Returns the full formatted list |
remove_todo(id) | Remove an item and clean up dangling dependency references |
list_todos(status_filter?) | Show all items, or filter by status |
get_next_todo() | Return the highest-priority pending item whose dependencies are all in terminal status |
finish_task(summary, status) | Explicitly signal task completion (completed/blocked/failed) |
Statuses
| Status | Terminal? | Icon | Description |
|---|---|---|---|
pending | No | [ ] | Not started |
in_progress | No | [>] | Currently being worked on |
completed | Yes | [x] | Successfully finished |
failed | Yes | [!] | Failed |
skipped | Yes | [-] | Intentionally skipped |
Priority and Dependencies
Priority ordering: critical > high > medium > low. get_next_todo() returns the highest-priority pending item whose dependencies are all in terminal status.
Items can depend on other items by ID. In batch creation, use 0-based indices as dependency refs. Cycles are detected via Kahn's algorithm and rejected immediately.
Auto-Completion
When every item in the list reaches a terminal status (completed, failed, or skipped), the autonomous loop automatically signals completion. The agent does not need to call finish_task explicitly, though it can do so at any time to override.
Shared Mode
When shared: true, the todo list is backed by SQLite with WAL mode for concurrent access. Sub-agents spawned via the spawn tool can read and update the same list.
tools:
- type: todo
shared: true
shared_path: ./.initrunner/shared_todo.dbWhen to Use
Add the todo tool for agents that need to track multi-step work:
- Autonomous agents: structured task tracking with automatic completion detection.
- Todo-driven reasoning: pair with
spec.reasoning.pattern: todo_drivenfor plan-first execution. See Reasoning Primitives. - Multi-agent coordination: enable
shared: trueso spawned sub-agents can update the same list.
Example
# Autonomous agent with structured task tracking
spec:
role: |
You are a project planner. Break tasks into structured
todo lists and work through each item systematically.
model:
provider: openai
name: gpt-5-mini
tools:
- type: think
critique: true
- type: todo
max_items: 20
reasoning:
pattern: todo_driven
auto_plan: true
autonomy:
max_plan_steps: 20
guardrails:
max_iterations: 15
autonomous_token_budget: 100000Spawn Tool
Non-blocking parallel agent execution. Spawn sub-agents as background tasks, poll for results, and await completion, all within a single agent run.
tools:
- type: spawn
max_concurrent: 3
timeout_seconds: 120
agents:
- name: researcher
role_file: ./agents/researcher.yaml
description: Researches a specific topicOptions
| Field | Type | Default | Description |
|---|---|---|---|
agents | list | required | Agent refs with name, role_file or url, and description |
max_concurrent | int | 4 | Maximum parallel tasks (1–16) |
max_depth | int | 3 | Maximum delegation depth |
timeout_seconds | int | 300 | Per-task wall-clock timeout |
shared_memory | object | null | Shared LanceDB memory config |
Each agent ref needs either role_file (inline execution) or url (remote execution via MCP).
Since v2026.6.5, max_depth is enforced across spawned sub-agents: delegation depth travels on context variables and is re-seeded across the spawn pool's thread boundary. Earlier it was thread-local and reset to zero on each worker thread, so a recursive spawn topology could exceed the limit. The default value is unchanged.
Registered Functions
| Tool | Description |
|---|---|
spawn_agent(agent_name, prompt) | Submit a background task. Returns immediately with a task_id |
poll_tasks(task_ids?) | Check status of specific tasks or all. Returns a formatted status table |
await_tasks(task_ids) | Block until all specified tasks complete. Returns their results |
await_any(task_ids) | Block until any one task completes. Returns its result |
cancel_task(task_id) | Cancel a running background task |
Task statuses: running, completed, failed, timeout.
When to Use
- Parallelizable research: spawn multiple researchers for different topics simultaneously.
- Fan-out/gather: distribute work across specialist agents and synthesize results.
- Long-running sub-tasks: offload heavy work to background agents while the coordinator continues.
See Reasoning Primitives for how to compose the spawn tool with todo-driven strategies.
Example
# Coordinator with parallel sub-agents
spec:
role: |
You are a research lead. Spawn researchers for different topics
and synthesize their findings into a report.
model:
provider: openai
name: gpt-5-mini
tools:
- type: todo
- type: spawn
max_concurrent: 3
agents:
- name: web-researcher
role_file: ./agents/web-researcher.yaml
description: Searches the web and summarizes findings
- name: data-analyst
role_file: ./agents/data-analyst.yaml
description: Analyzes data and produces charts
reasoning:
pattern: todo_driven
auto_plan: trueBlackboard Tool
A blackboard is a small per-run key-value store with provenance, giving a flow a typed side channel that survives fan-out and is readable at the fan-in join. An upstream agent posts a value under a key, and a downstream agent (or the join) reads it back by the same key instead of threading it through prompt text. Each entry records its author and an ISO-8601 UTC timestamp.
The tool is run-scoped and flow-only. It is built fresh for each agent step with the flow's live board injected, the same way todo receives fresh run-scoped state. Outside a flow there is no board, so the tool is never built.
tools:
- type: blackboard
max_entries: 50
max_value_chars: 10000Declaring and reading the board
Declare type: blackboard on each flow agent that should read or write shared state. An agent only gets the post, read, claim, and list functions if its role declares the tool; the common case adds nothing to the run.
The board is not silently injected into every agent. Two narrower behaviors are automatic:
- The flow runner builds the toolset run-scoped with the live board injected, rather than at agent-build time.
- The fan-in join folds still-posted (unclaimed) entries into the downstream agent's input under a
=== Shared blackboard ===section, attributed by author. This happens even for join-target agents that did not declare the tool. Claimed entries are gone and do not reappear.
For an agent to post or claim entries itself, it must declare the tool. The join surfaces still-posted entries to downstream agents automatically.
Options
| Field | Type | Default | Description |
|---|---|---|---|
max_entries | int | 100 | Board capacity for the run, range 1 to 1000. A full board rejects further posts until an entry is claimed |
max_value_chars | int | 10000 | Per-value size cap in characters, range 1 to 100000. Post JSON when you need structure |
Registered Functions
| Tool | Description |
|---|---|
blackboard_post(key, value) | Add a new entry. Keys are letters, digits, and underscore up to 64 chars. Posting an existing key is an error, so claim it first to replace |
blackboard_read(key) | Return the entry as JSON (key, value, author, timestamp, entry_id) without removing it |
blackboard_claim(key) | Read and remove the entry so no other agent can claim it again. Use this for work-stealing handoffs |
blackboard_list() | List current keys with a short value preview, truncated at 80 chars. An empty board returns Blackboard is empty. |
When to Use
- Structured handoff: a planner posts a decision or plan that downstream workers read back exactly, rather than re-parsing prose.
- Join on a computed value: a fan-in join merges based on a value an upstream branch computed.
- Work stealing: one of several parallel workers claims a unit of work with
blackboard_claimso no sibling also takes it.
If agents only need to pass prose forward, the default prompt concatenation at the join already covers it.
On flow completion the final board is recorded on the signed audit chain via a blackboard_state entry. Nothing is written when the board never held an entry. See Observability for querying the audit chain.
See Blackboard for the coordination model and Reasoning Primitives for composing it with other run-scoped tools.
Script Tool
Defines inline shell scripts in YAML as named, parameterized agent tools. Each script becomes a separate tool function with typed parameters. Script bodies are piped to an interpreter via stdin, with no temporary files and no shell=True.
tools:
- type: script
interpreter: /bin/sh # default interpreter
timeout_seconds: 30 # default timeout per script
max_output_bytes: 102400 # default: 100 KB
working_dir: null # default: role directory
scripts:
- name: disk_usage
description: Check disk usage for a path
interpreter: /bin/bash # override per script
body: |
df -h "$TARGET_PATH"
parameters:
- name: target_path
description: Filesystem path to check
required: trueTop-Level Options
| Field | Type | Default | Description |
|---|---|---|---|
scripts | list[ScriptDefinition] | (required) | One or more script definitions. Names must be unique. |
interpreter | str | "/bin/sh" | Default interpreter for scripts that don't specify their own. |
timeout_seconds | int | 30 | Default timeout for scripts that don't specify their own. |
max_output_bytes | int | 102400 | Maximum output size (100 KB). Truncated output includes a [truncated] marker. |
working_dir | str | null | null | Working directory for all scripts. null uses the role file's directory. |
Script Definition
| Field | Type | Default | Description |
|---|---|---|---|
name | str | (required) | Tool function name. Must be a valid Python identifier. |
description | str | "" | Tool description shown to the LLM. Falls back to "Run the '<name>' script". |
body | str | (required) | The script source. Piped to the interpreter via stdin. Must not be empty. |
interpreter | str | null | null | Override the top-level interpreter for this script. null inherits from parent. |
parameters | list[ScriptParameter] | [] | Parameters injected as uppercase environment variables. |
timeout_seconds | int | null | null | Override the top-level timeout for this script. null inherits from parent. |
allowed_commands | list[str] | [] | When non-empty, validates that every command line in the body uses one of these commands. Empty list skips validation. |
Script Parameter
| Field | Type | Default | Description |
|---|---|---|---|
name | str | (required) | Parameter name. Must be a valid Python identifier. Injected as NAME (uppercased) in the subprocess environment. |
description | str | "" | Parameter description for the LLM. |
required | bool | false | Whether the parameter is required. |
default | str | "" | Default value for optional parameters. |
Parameter Injection
Parameters are injected as uppercase environment variables. A parameter named target_path becomes $TARGET_PATH in the script body:
parameters:
- name: target_path
description: Filesystem path to check
required: true# In the script body:
df -h "$TARGET_PATH"Default values are always applied to the environment, so scripts work correctly even when the LLM omits optional parameters.
Security
- No
shell=True: scripts are piped to the interpreter via stdin, not passed through a shell. - Env scrubbing: sensitive environment variables (
OPENAI_API_KEY,AWS_SECRET, etc.) are removed from the subprocess environment. - Output bounded: output exceeding
max_output_bytesis truncated with a[truncated]marker. - Timeout enforcement: scripts that exceed their timeout are killed and a
SubprocessTimeouterror is raised. - Working directory isolation: when
working_diris set, all scripts execute in that directory. Falls back to the role file's directory. - Runtime sandbox: when
security.sandbox.backendis set tobwrap,docker, orauto, scripts run inside the resolved backend. See Runtime Sandbox.
Examples
Single-command scripts with allowed_commands:
tools:
- type: script
scripts:
- name: disk_usage
description: Check disk usage for a path
allowed_commands: [df]
body: |
df -h "$TARGET_PATH"
parameters:
- name: target_path
required: trueMulti-command scripts (no allowed_commands, trusting the role author):
tools:
- type: script
scripts:
- name: system_info
description: Show basic system information
interpreter: /bin/bash
body: |
echo "Hostname: $(hostname)"
echo "Kernel: $(uname -r)"
echo "Uptime: $(uptime -p 2>/dev/null || uptime)"
echo "Memory:"
free -h 2>/dev/null || echo "free not available"Python interpreter:
tools:
- type: script
scripts:
- name: calculate
description: Evaluate a math expression
interpreter: python3
body: |
import os, ast
print(ast.literal_eval(os.environ["EXPR"]))
parameters:
- name: expr
description: Math expression to evaluate
required: trueAuto-Registered Tools
Document Search (from ingest)
When spec.ingest is configured, a search_documents tool is auto-registered:
search_documents(query: str, top_k: int = 5, source: str | None = None) -> strquery: natural-language search string (embedded and compared against stored chunks).top_k: number of results to return (default5).source: optional glob pattern to filter results by source file path (e.g."*billing*").
See Ingestion for full details and the RAG Patterns Guide for usage examples.
Memory Tools (from memory)
When spec.memory is configured, up to five tools are auto-registered depending on which memory types are enabled: remember(content, category), recall(query, top_k, memory_types), list_memories(category, limit, memory_type), learn_procedure(content, category), and record_episode(content, category). See Memory.
Plugin Tools
Third-party packages can register new tool types via the initrunner.tools entry point. Once installed (pip install initrunner-<name>), the new type is available in spec.tools like any built-in.
List discovered plugins with initrunner plugins.
Note: Plugin tools do not support the
permissionsblock. The plugin parser strips non-typekeys into a genericconfigdict, sopermissionsis silently ignored. This is a known limitation.
Async Tool Execution
When running inside Flow or the API layer, agents are built with prefer_async=True. This gives I/O-bound tools async closures that run natively on the asyncio event loop without thread-pool overhead.
| Tool | Async Behavior |
|---|---|
http | Uses httpx.AsyncClient with SSRF-safe transport |
web_reader | Async fetch and markdown conversion |
web_scraper | Async fetch + concurrent embeddings via asyncio.gather |
search | Async HTTP for search APIs |
Inherently blocking tools (filesystem, script, shell, sql, git) ignore prefer_async since their I/O is CPU-bound or uses blocking libraries. A custom tool may be def or async def: a synchronous function is auto-wrapped in run_in_executor when running in an async context, while an async def function (the default for tool new scaffolds) runs natively on the event loop.
Resource Limits
| Tool | Limit | Behavior |
|---|---|---|
read_file | 1 MB | Truncated with [truncated] note |
http_request | 100 KB | Truncated with [truncated] note |
git_* | 100 KB | Truncated with recovery hint |