Table of contents
Defining Custom Tools
Custom tools allow you to compose existing tools and logic into reusable components. Use the input, tools, and do sections:
tools:
- name: my_custom_tool
description: "Description of what this tool does"
input:
- name: name
description: "Description of first parameter"
type: "str"
default: "default value" # Optional
- name: param2
description: "Description of second parameter"
type: "int"
tools: # Optional, local tools available only within this custom tool
- import_tool: llm_workers.tools.fs.read_file
return_direct: true # Optional, returns result directly to user
do:
- call: read_file # References local tool
params:
path: ${name}
- if: ${len(_).trim() == 0}
then:
eval: "File ${name} is empty"
else:
eval: ${_}
Key Sections:
input: Defines the parameters that the tool accepts. These parameters can be referenced in thedosection using the${param_name}syntax.tools: Optional list of tools available only within this custom tool. These are “local tools” that don’t pollute the global namespace.do: Contains one or more statements that define the tool’s behavior.
Tool Scoping: Custom tools create their own tool scope. Tools defined in the tools field are only accessible within that custom tool’s do section. Local tools can shadow global tools with the same name.
The do section contains one or more statements that can be composed in various ways:
call Statement
Executes a specific tool with optional parameters. Tools must be defined in a tools section before they can be called.
Syntax:
- call: tool_name
params:
param1: value1
param2: value2
catch: [error_type1, error_type2] # Optional error handling
store_as: result_var # Optional, stores result in a named variable
ui_hint: 'Processing ${param1}' # Optional, overrides the tool's UI hint
Parameters:
call: Name of the tool to executeparams: Dictionary of parameters to pass to the toolcatch: (Optional) List of error types to catch and handle gracefullystore_as: (Optional) Variable name to store the result. Can be referenced later using${variable_name}ui_hint: (Optional) Override the tool’s built-in UI hint with a custom message. Supports template variables from the current evaluation context (e.g.,${param_name},${_})
Example:
shared:
tools:
- import_tool: llm_workers.tools.fs.read_file
- name: process_file
input:
- name: path
type: str
tools:
- import_tool: llm_workers.tools.fs.read_file # Local tool
do:
- call: read_file # References local tool
params:
path: "${path}"
- eval: "Processed: ${_}"
Example with store_as:
tools:
- name: multi_step_process
input:
- name: script
type: str
- name: approval_token
type: str
do:
- call: validate_approval
params:
approval_token: "${approval_token}"
- call: run_python_script
params:
script: "${script}"
store_as: script_result # Store result in named variable
- call: consume_approval
params:
approval_token: "${approval_token}"
- eval: "${script_result}" # Reference stored result
Example with ui_hint:
tools:
- name: check_translations
description: "Checks translations for a given key"
input:
- name: key
type: str
- name: locale
type: str
do:
- call: fetch_translations
params:
key: "${key}"
locale: "${locale}"
ui_hint: 'Checking translations for key "${key}" (${locale})'
This will display Checking translations for key "welcome_message" (en) in the UI instead of the default tool hint when called with key="welcome_message" and locale="en".
Note: Without store_as, the result of each statement is available as ${_} in the next statement. With store_as, you can reference the result by name later in the workflow, which is useful when you need to perform multiple operations and reference earlier results.
Important Changes: As of recent versions, inline tool definitions in call statements are no longer supported. Tools must be defined in a tools section (either in shared.tools, chat.tools, custom tool’s tools, or CLI’s tools) before they can be referenced by name in call statements.
For custom tools: Define local tools in the custom tool’s tools field For global access: Define tools in shared.tools For chat/CLI: Define tools in chat.tools or cli.tools
This separation provides:
- Clearer distinction between tool definition and tool usage
- Better tool scoping and encapsulation
- Support for local tools that don’t pollute the global namespace
eval Statement
The eval statement evaluates an expression and returns the result. It supports all Simple Eval features including nested access, function calls, and conditional expressions.
Basic Usage
- eval: "This is a fixed response"
- eval:
status: success
data:
value: 42
Dictionary Access
# Static key
- eval: "${data['field_name']}"
- eval: "${data.field_name}"
# With default
- eval: "${data['field_name'] if 'field_name' in data else 'default_value'}"
# or
- eval: "${get(data, 'field_name', 'default_value')}"
# Dynamic key from variable
- eval: "${data[key_name]}"
# Nested
- eval: "${get(get(config, 'database', {}), 'host', 'localhost')}"
List Access
# Direct indexing
- eval: "${items[0]}"
- eval: "${items[-1]}"
# With bounds checking
- eval: "${items[index] if 0 <= index < len(items) else 'default'}"
# or
- eval: "${get(items, index, 'default')}"
Conditional Expressions
- eval: "${value if value is not None else 'N/A'}"
- eval: "${'valid' if score >= 80 else 'invalid'}"
- eval: "${'A' if score >= 90 else 'B' if score >= 80 else 'C'}"
Expression Composition
# String interpolation
- eval: "User ${user_name} has score ${score}"
# Arithmetic
- eval: "${total_price * 1.15}" # Add 15% tax
# String operations
- eval: "${name.upper()}"
# List operations
- eval: "${len(items)}"
- eval: "${[x * 2 for x in numbers]}" # List comprehension
if Statement
Executes different actions based on a boolean condition:
- if: "${condition}"
then:
<statement(s)> # Executed if condition is truthy
else: # Optional
<statement(s)> # Executed if condition is falsy
store_as: result_var # Optional, stores the result of the executed branch
Parameters:
if: Boolean condition expression to evaluatethen: Statement(s) to execute if condition is truthyelse: (Optional) Statement(s) to execute if condition is falsystore_as: (Optional) Variable name to store the result of whichever branch executes
Basic Examples
Simple boolean check:
- if: "${user_authenticated}"
then:
call: fetch_user_data
params:
user_id: "${user_id}"
else:
eval: "Please log in first"
Boolean expression with comparison:
- if: "${score >= 80 and status == 'active'}"
then:
eval: "Eligible for promotion"
else:
eval: "Not eligible"
Membership test:
- if: "${movie_title in stub_data}"
then:
eval: "${stub_data[movie_title]}"
else:
call: fetch_from_api
params:
query: "${movie_title}"
Optional else clause:
- if: "${debug_mode}"
then:
call: log_debug_info
params:
message: "Debug enabled"
# If debug_mode is false, returns None and continues
Multiple statements in branches:
- if: "${needs_preprocessing}"
then:
- call: normalize_data
params:
data: "${input}"
- call: validate_data
params:
data: "${_}"
else:
eval: "${input}"
Nested if statements:
- if: "${user_role == 'admin'}"
then:
- if: "${action == 'delete'}"
then:
call: delete_resource
params:
id: "${resource_id}"
else:
eval: "Action not allowed"
else:
eval: "Admin access required"
Truthiness Rules
The condition uses Python truthiness rules:
- Truthy values: Non-empty strings, non-zero numbers, non-empty lists/dicts,
True - Falsy values: Empty string
"", zero0,False, empty lists[], empty dicts{}
Note: Direct evaluation of None variables is not supported due to expression system limitations. Use explicit comparisons like ${value is not None} when checking for None.
for_each Statement
Iterates over a collection and executes the body for each element:
- for_each: ${collection}
parallelism: 4 # Optional, enables parallel execution
do:
<statement(s)> # Executed for each element
store_as: result_var # Optional, stores the final result
Parameters:
for_each: Expression that evaluates to a list, dict, or scalar valuedo: Statement(s) to execute for each elementparallelism: (Optional, default 0) Number of parallel workers0or1: Sequential execution (default behavior)>1: Parallel execution with N worker threads
store_as: (Optional) Variable name to store the result
Behavior by Input Type:
| Input Type | Output Type | Available Variables |
|---|---|---|
| Dict | Dict with same keys, mapped values | _ = current value, key = current key |
| Iterable (list, tuple, set, etc.) | List of results | _ = current element |
| Other (str, int, None, etc.) | Single result | _ = the scalar value |
Note: Strings are treated as scalars, not as character iterables.
Basic Examples
Iterating over a list:
- for_each: ${names}
do:
eval: "Hello, ${_}!"
With input ["Alice", "Bob"], returns ["Hello, Alice!", "Hello, Bob!"].
Iterating over a dict:
- for_each: ${users}
do:
eval: "User ${key} is ${_}"
With input {"id1": "Alice", "id2": "Bob"}, returns {"id1": "User id1 is Alice", "id2": "User id2 is Bob"}.
Scalar passthrough:
- for_each: ${single_value}
do:
eval: "Value: ${_}"
With input 42, returns "Value: 42".
Other iterables (tuple, set, etc.):
- for_each: ${items}
do:
eval: "${_ * 2}"
With input (1, 2, 3) (tuple) or {1, 2, 3} (set), returns [2, 4, 6] (always a list).
Advanced Examples
Multi-statement body:
- for_each: ${numbers}
do:
- eval: "${_ * 2}"
store_as: doubled
- eval: "${doubled + 1}"
With input [1, 2, 3], returns [3, 5, 7].
Nested for_each:
- for_each: ${matrix}
do:
for_each: ${_}
do:
eval: "${_ * 10}"
With input [[1, 2], [3, 4]], returns [[10, 20], [30, 40]].
With tool calls:
- for_each: ${file_paths}
do:
call: read_file
params:
path: "${_}"
lines: 1000
Accessing parent context:
- for_each: ${items}
do:
eval: "${prefix}: ${_}"
Variables from the parent context (like ${prefix}) remain accessible inside the loop body.
Storing results:
- for_each: ${items}
do:
eval: "${_ * 2}"
store_as: doubled_items
- eval: "Processed ${len(doubled_items)} items"
Parallel file processing:
- for_each: ${file_paths}
parallelism: 4
do:
call: read_file
params:
path: "${_}"
Parallel execution processes items concurrently using multiple worker threads, which can significantly speed up I/O-bound operations like file reading or API calls. Results are always returned in the original order regardless of execution order.
Edge Cases
- Empty list: Returns
[] - Empty dict: Returns
{} - None input: Applies body to
None, returns single result
Composing Statements
Statements can be used as part of a tool’s body section to create composite tools:
tools:
- name: advanced_search
description: "Performs an enhanced search with preprocessing"
input:
- name: query
type: str
do:
- call: normalize_input
params:
text: "{query}"
- call: search_database
params:
query: "${_}"
- if: "${_ == ''}"
then:
eval: "No results found"
else:
eval: "${_}"
Template Variables
Custom tools support template variables using the ${...} expression syntax (powered by simpleeval):
- Direct references:
"${param_name}"- preserves referenced parameter type (returns the actual value, not string) - Complex templates:
"The value is ${param_name} and ${other_param}"- returns string with interpolated values - Nested element access:
- Dictionary keys (bracket):
"${param_dict['key_name']}"- accesses dictionary values by key using standard Python syntax - Dictionary keys (dot):
"${param_dict.key_name}"- also works via simpleeval’s “sweetener” feature - List indices:
"${param_list[0]}"- accesses list elements by index - Nested structures:
"${param_dict['nested']['value']}"or"${param_dict.nested.value}"- supports multiple levels of nesting
- Dictionary keys (bracket):
- Shared data variables:
"${key}"- accesses variables defined in theshared.datasection - Tool input parameters:
"${param_name}" - (inside the list of statements) Previous statement results:
"${_}"for the immediate previous result - Python expressions:
"${a + b}","${len(items)}","${value if condition else default}"- supports any safe Python expression via simpleeval
Type Preservation: When a string contains only a single expression (e.g., "${param}"), the original type is preserved. When text or multiple expressions are present, the result is converted to a string.
Note: Expressions are evaluated using simpleeval for safety, which supports standard Python operations but restricts potentially dangerous operations.
Example with nested element and shared variables:
shared:
data:
app:
name: "MyApp"
version: "1.0"
templates:
user_format: "Welcome to ${app['name']}!"
tools:
- name: process_user_data
description: "Processes user data with nested access"
input:
- name: user_profile
description: "User profile object"
type: object
- name: settings
description: "User settings array"
type: array
do:
- eval: "User ${user_profile['name']} has email ${user_profile['contact']['email']} and first setting is ${settings[0]}. ${templates['user_format']}"
This would process input like:
{
"user_profile": {
"name": "John",
"contact": {"email": "john@example.com"}
},
"settings": ["dark_mode", "notifications"]
}
And return: "User John has email john@example.com and first setting is dark_mode. Welcome to MyApp!"
Important: Note that shared variables are accessed directly (e.g., ${app}, ${templates}) without a shared. prefix. This is different from earlier versions where you needed to use ${shared.app} or ${shared['app']}.