Skip to content

cap_lua and Lua overview

Source: cap_lua.ccomponents/claw_capabilities/cap_lua/src/cap_lua.c · header: cap_lua.hcomponents/claw_capabilities/cap_lua/include/cap_lua.h

ESP-Claw uses Lua as the primary on-device language for programmable automation. It plays three roles:

The LLM can author scripts with file tools (write_file) and run them (lua_run_script). Lua bridges natural-language plans to hardware: GPIO, display, audio, etc.

LLM decides → emits Lua → write_file saves under scripts/ → lua_run_script runs → hardware reacts

claw_event_router supports a run_script action so rules can fire Lua without going through an LLM:

{
  // Some fields (for example, id) are omitted
  "match": { "event_type": "button_pressed" },
  "actions": [{ "type": "run_script", "input": { "path": "on_press.lua" } }]
}

That yields fully local, low-latency responses without network or model cost.

Users who do not ship C firmware can still extend behavior via Lua. Native modules registered as lua_module_* / lua_driver_* expose peripherals through small Lua APIs.

ESP-Claw runs Lua via cap_lua_runtime_* helpers. The runtime initializes in cap_lua_group_init, locks module registration, then starts.

Writable cap_lua runtime scripts must live under the Script Base Dir. In edge_agent, the default Script Base Dir is /fatfs/scripts/ (apps can override it via cap_lua_set_base_dir). Read-only scripts bundled with Skills may run from /fatfs/skills/<skill_id>/scripts/....

  • path must target a .lua file.
  • path supports two forms:
    • Managed relative path: e.g. hello.lua or builtin/test/hello.lua (auto-expanded under ${base_dir} and must not contain ..).
    • Skill-local absolute path: e.g. {CUR_SKILL_DIR}/scripts/hello.lua when an active skill documents that script. It resolves under /fatfs/skills/<skill_id>/scripts/..., must stay under the Skill root, and must not contain ...
  • Discover and read scripts with file tools: list_dir {"keyword":"scripts/"} and read_file {"path":"scripts/hello.lua"}.

cap_lua_register_module is only valid before cap_lua_group_init. After init, s_module_registration_locked = true and further registration returns ESP_ERR_INVALID_STATE, freezing the module set at boot.

// During app init (before cap_lua_register_group)
lua_module_display_register();   // register display
lua_driver_gpio_register();      // register gpio
// ...
cap_lua_register_group("/fatfs/scripts");  // pass script base dir, then lock

cap_lua currently registers six Callables:

Tool IDPurpose
lua_run_scriptSync run; block until output
lua_run_script_asyncAsync enqueue; returns job_id immediately
lua_list_async_jobsList async jobs (status filter)
lua_get_async_jobFetch status/output for one job (supports lookup by job_id or name)
lua_stop_async_jobStop one async job (by job_id or name)
lua_stop_all_async_jobsStop async jobs in bulk (optionally filter by exclusive group)
ModeWhen to useTimeoutReturns
lua_run_scriptQuick work / state readsoptional timeout_msscript output string
lua_run_script_asyncLong work (animation, waiting on sensors)timeout_ms=0 means run until stopped (default)job_id

lua_run_script_async supports name, exclusive, and replace controls:

  • name: assign a logical name for later lua_get_async_job / lua_stop_async_job operations.
  • exclusive: mutual-exclusion group (for example, "display"), commonly used for single-slot resources.
  • replace: true: when an active job with the same name or exclusive group exists, attempt to preempt and replace it.

Typical flow:

// LLM call (start a long-running job):
{"path": "animation.lua", "name": "clock_anim", "exclusive": "display", "timeout_ms": 0}

// Immediate return:
"Started Lua job 8a72f3c1 (name=clock_anim, exclusive=display, timeout_ms=0 [until cancelled], status=running) for animation.lua"

// Later query:
{"name": "clock_anim"}
// Response: includes job_id / status / summary, etc.

// Stop the job:
{"name":"clock_anim"}

Optional JSON args becomes the global args inside the script:

-- read parameters in-script
local input = args  -- mirrors caller args object
local speed = input.speed or 100

When lua_run_script / lua_run_script_async is triggered by the Agent inside an IM conversation, if the tool call does not explicitly provide args.channel, args.chat_id, or args.session_id, the runtime auto-merges the current session context into args. This lets scripts read conversation context directly (for example, for replies) without passing these fields every time.

{"path": "scripts/hello.lua", "content": "print('hello')"}

Use write_file to create or overwrite Lua scripts under the file capability base dir. Then run the same script through lua_run_script with the leading scripts/ removed, for example {"path":"hello.lua"}.

  • Lua timeout detection combines an instruction-hook check (triggered every fixed instruction count) with wall-clock timeout; when timed out, it throws execution timed out.
  • The hook callback proactively calls taskYIELD() to avoid tight loops monopolizing CPU and accidentally triggering the task watchdog.
  • Integer-like values in JSON args are preserved as Lua integer type where possible, reducing type ambiguity for GPIO values, pixel coordinates, and similar parameters.

Besides tools, cap_lua exposes C helpers for firmware code:

// sync run
cap_lua_run_script("startup.lua", NULL, 3000, output, sizeof(output));

// async run (optional name / exclusive / replace)
cap_lua_run_script_async("startup.lua",
                         NULL,
                         0,
                         "startup_loop",
                         "display",
                         false,
                         output,
                         sizeof(output));

// stop one async job (job_id or name)
cap_lua_stop_job("startup_loop", 2000, output, sizeof(output));

// stop all async jobs in a group
cap_lua_stop_all_jobs("display", 2000, output, sizeof(output));

// register module (must be before cap_lua_register_group)
cap_lua_register_module("my_module", luaopen_my_module);

Jobs move through:

Diagram

lua_list_async_jobs filters with "all" / "queued" / "running" / "done" / "failed" / "timeout" / "stopped".

The cap_lua Skill instructs the model to inspect running Lua async jobs with lua_list_async_jobs / lua_get_async_job before stopping, replacing, or deactivating long-lived work.

Lua extension modules How `lua_module_*` / `lua_driver_*` registers, how to build your own, and a deep dive on `display`