chapter 5
Build Your Own Tool
You have two paths: runtime user tools (fast, no reflash) and firmware-coded tools (C handlers, rebuild, reflash).
Two Approaches
| Approach | Best for | Cost |
|---|---|---|
Runtime user tool (create_tool) | Fast macros built from existing built-in tools. | No code changes, no reflash. |
| Firmware tool (new C handler) | New capabilities, strict validation, external integrations. | Code + tests + rebuild/reflash. |
Approach 1: Runtime User Tool (No Reflash)
A user tool stores a short action string. Later, when the model calls that tool, zclaw returns the action text and the model executes it with built-in tools.
1) create_tool(name, description, action) 2) Tool is saved in NVS 3) Model calls tool by name 4) Firmware returns: "Execute this action now: ..." 5) Model performs gpio/memory/cron calls
No dynamic code loading. You are shaping behavior through promptable intent.
Approach 1 Contract
| Field | Rule | Notes |
|---|---|---|
name | Alphanumeric + underscore only | No spaces or punctuation. |
description | Short and explicit | Used by model selection logic. |
action | Concrete imperative text | Should map cleanly to built-in tools. |
| capacity | Up to 8 user tools | Delete stale tools to free slots. |
Approach 1 Workflow
- Pick one job with clear start/end conditions.
- Name it by intent:
water_plants,prep_night_mode,check_lab_sensor. - Write action text as ordered steps the model can execute.
- Create the tool in chat and immediately invoke it with a test prompt.
- Adjust wording if the model takes unexpected paths.
Concrete Examples
You: Create a tool named "water_plants". Description: Water plant zone A. Action: Set GPIO 5 high, wait 30000 milliseconds, then set GPIO 5 low. You: Run water_plants Agent: (calls tool) -> executes gpio_write + delay + gpio_write
You: Create tool "night_shutdown" to prepare lab for night. Action: Turn GPIO 7 low, remember u_last_shutdown=completed, and set a once schedule in 600 minutes to run morning_startup.
Memory-Driven Tool Creation
Tool creation gets stronger when zclaw already knows your hardware map from memory.
You: Remember that GPIO 5 controls the office lighting. Agent: Stored as memory key (for example: u_office_lighting_pin=5) Later... You: Build a tool to control the lights. Agent: Creates a tool using the remembered lighting pin instead of asking you to restate wiring.
Pattern: teach wiring once with memory, then create higher-level tools in plain language.
Approach 2: Firmware Tool (C Code + Reflash)
Use this path when you need behavior that is not a composition of existing tools (new I/O paths, strict schemas, deterministic validation, special error handling).
Example boundary: a user tool can compose i2c_write_read or dht_read, but it cannot invent a new bus or timing-sensitive sensor driver. That belongs in firmware.
- Implement handler logic in
main/tools_*.cwith signaturebool handler(const cJSON *input, char *result, size_t result_len). - Declare it in
main/tools_handlers.h. - Add one entry in
main/builtin_tools.def. - Add/extend host tests in
test/host/. - Build, flash, verify behavior on device.
TOOL_ENTRY("relay_status",
"Get relay health from host web relay.",
"{\"type\":\"object\",\"properties\":{}}",
tools_relay_status_handler)
Built-in registry is centralized in main/builtin_tools.def to make code-path tool additions one edit after handler implementation.
Method B Walkthrough (Step By Step)
Example: add a firmware tool named relay_status.
- Create a handler in
main/tools_*.cwith the standard signature.
// main/tools_relay.c
bool tools_relay_status_handler(const cJSON *input, char *result, size_t result_len)
{
(void)input;
snprintf(result, result_len, "Relay status: healthy");
return true;
}
- Declare the handler in
main/tools_handlers.h.
bool tools_relay_status_handler(const cJSON *input, char *result, size_t result_len);
- Register it in
main/builtin_tools.defso the model can discover and call it.
TOOL_ENTRY("relay_status",
"Get relay health from host web relay.",
"{\"type\":\"object\",\"properties\":{}}",
tools_relay_status_handler)
- Add host coverage in
test/host/for input validation and expected output shape. - Build and run through the normal firmware flow.
./scripts/test.sh host ./scripts/build.sh ./scripts/flash.sh --kill-monitor /dev/cu.usbmodem1101 ./scripts/monitor.sh /dev/cu.usbmodem1101
Treat name, description, and JSON schema as a model-facing API contract. Change deliberately and keep tests aligned.
How To Choose
- Choose Approach 1 if existing built-in tools can express the behavior.
- Choose Approach 2 if you need new capability, strict schema contracts, or firmware-level guarantees.
- Use Approach 1 when the flow is expressible as simple built-in tool calls (for example GPIO moves and delays), and use Approach 2 for more complex or custom tooling.
Validation Checklist
- Does the action text reference only supported built-in capabilities?
- Are durations explicit in milliseconds/minutes (avoid vague words like "later")?
- Does it avoid unsafe GPIO pins per your board policy? (The agent will generally prevent this anyway.)
- Can the outcome be observed via
gpio_read,memory_get, orcron_list?
Versioning and Retirement
Treat tool names like API contracts. If behavior changes meaningfully, create a new name and migrate callers.
# Discover current tools list_user_tools # Remove stale tool delete_user_tool(name="night_shutdown_v1")
Pattern: append a version suffix only when compatibility breaks (_v2, _v3).
Failure Modes To Watch
- Overly broad actions that invite hallucinated extra steps.
- Tool names that overlap semantically and confuse model choice.
- Implicit assumptions about hardware state (pin mode, sensor presence, wiring).
- Long multi-domain macros that should be split into smaller tools.