zclaw docs

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

ApproachBest forCost
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

FieldRuleNotes
nameAlphanumeric + underscore onlyNo spaces or punctuation.
descriptionShort and explicitUsed by model selection logic.
actionConcrete imperative textShould map cleanly to built-in tools.
capacityUp to 8 user toolsDelete stale tools to free slots.

Approach 1 Workflow

  1. Pick one job with clear start/end conditions.
  2. Name it by intent: water_plants, prep_night_mode, check_lab_sensor.
  3. Write action text as ordered steps the model can execute.
  4. Create the tool in chat and immediately invoke it with a test prompt.
  5. 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.

  1. Implement handler logic in main/tools_*.c with signature bool handler(const cJSON *input, char *result, size_t result_len).
  2. Declare it in main/tools_handlers.h.
  3. Add one entry in main/builtin_tools.def.
  4. Add/extend host tests in test/host/.
  5. 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.

  1. Create a handler in main/tools_*.c with 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;
}
  1. Declare the handler in main/tools_handlers.h.
bool tools_relay_status_handler(const cJSON *input, char *result, size_t result_len);
  1. Register it in main/builtin_tools.def so 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)
  1. Add host coverage in test/host/ for input validation and expected output shape.
  2. 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, or cron_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.