Skip to main content
Actions are the core of RCLI’s macOS integration. Each action is a C++ function that executes via AppleScript, shell commands, or native APIs, and is automatically exposed to the LLM for tool calling.

Action System Overview

RCLI’s action system consists of:
  • ActionRegistry — Registers actions and dispatches execution
  • ActionDef — Defines action metadata (name, description, JSON schema)
  • ActionFunc — Function that executes the action and returns a result
  • Tool Engine — Parses LLM tool calls and routes to the registry
Actions live in src/actions/ and are registered in src/actions/action_registry.cpp.

Action Anatomy

Every action consists of three parts:
  1. Implementation function — executes the action
  2. Registration call — registers with the registry
  3. JSON schema — defines parameters for the LLM

Example: Create Note Action

Here’s the create_note action from src/actions/notes_actions.cpp:
src/actions/notes_actions.cpp
inline ActionResult action_create_note(const std::string& args_json) {
    // Parse JSON parameters
    std::string title = json_get_string(args_json, "title");
    std::string body  = json_get_string(args_json, "body");
    std::string folder = json_get_string(args_json, "folder");

    if (title.empty() && body.empty())
        return {false, "", "Title or body required", "{\"error\": \"missing content\"}"};

    // Try memo CLI if available
    auto check = run_shell("which memo 2>/dev/null");
    if (check.success) {
        std::string cmd = "printf '%s' '" + escape_shell(body.empty() ? title : body)
                         + "' | memo notes -a '" + escape_shell(title) + "'";
        auto r = run_shell(cmd);
        if (r.success)
            return {true, "Created note: " + title, "",
                    "{\"action\": \"create_note\", \"title\": \"" + title + "\", \"status\": \"created\"}"};
    }

    // Fallback to AppleScript
    std::string script =
        "tell application \"Notes\"\n"
        "  tell folder \"" + escape_applescript(folder.empty() ? "Notes" : folder) + "\"\n"
        "    make new note with properties {name:\"" + escape_applescript(title) +
        "\", body:\"" + escape_applescript(body.empty() ? title : body) + "\"}\n"
        "  end tell\n"
        "end tell";

    auto r = run_applescript(script);
    if (r.success)
        return {true, "Created note: " + title, "",
                "{\"action\": \"create_note\", \"title\": \"" + title + "\", \"status\": \"created\"}"};
    return {false, "", "Failed to create note: " + r.error, "{\"error\": \"" + r.error + "\"}"};
}

void register_notes_actions(ActionRegistry& registry) {
    registry.register_action(
        {"create_note", "Create a new note in Apple Notes",
         "{\"title\": \"note title\"}",
         true,
         "productivity",
         "Create a note called Meeting Notes",
         "rcli action create_note '{\"title\": \"Meeting Notes\"}'"},
        action_create_note);
}

Step-by-Step: Adding a New Action

1

Choose a category

Actions are organized by category in src/actions/:
  • notes_actions.cpp — Apple Notes integration
  • reminders_actions.cpp — Reminders integration
  • messages_actions.cpp — Messages/iMessage
  • app_control_actions.cpp — Open/quit apps
  • window_actions.cpp — Window management
  • system_actions.cpp — System settings (volume, dark mode, lock screen)
  • media_actions.cpp — Spotify/Apple Music
  • web_actions.cpp — Web search
  • browser_actions.cpp — Safari/Chrome control
  • clipboard_actions.cpp — Clipboard read/write
  • files_actions.cpp — File search
  • navigation_actions.cpp — Maps integration
  • communication_actions.cpp — FaceTime
Pick an existing file or create a new one (e.g., calendar_actions.cpp).
2

Write the action function

Create a function that matches the ActionFunc signature:
src/actions/my_category_actions.cpp
#include "actions/action_helpers.h"
#include "actions/applescript_executor.h"

namespace rcli {

inline ActionResult action_my_feature(const std::string& args_json) {
    // 1. Parse JSON parameters
    std::string param1 = json_get_string(args_json, "param1");
    std::string param2 = json_get_string(args_json, "param2");

    // 2. Validate inputs
    if (param1.empty())
        return {false, "", "param1 is required", "{\"error\": \"missing param1\"}"};

    // 3. Execute via AppleScript, shell, or native API
    std::string script = R"(
        tell application "MyApp"
            -- do something
        end tell
    )";

    auto r = run_applescript(script);

    // 4. Return success or failure
    if (r.success)
        return {true, "Action completed", "", "{\"status\": \"ok\"}"};
    return {false, "", "Action failed: " + r.error, "{\"error\": \"" + r.error + "\"}"};
}

} // namespace rcli
3

Register the action

Add a registration function in the same file:
void register_my_category_actions(ActionRegistry& registry) {
    registry.register_action(
        ActionDef{
            "my_feature",                           // Action name (used by LLM)
            "Description of what it does",          // Human-readable description
            "category",                             // Category (productivity, media, system, etc.)
            R"({"type":"object","properties":{"param1":{"type":"string"},"param2":{"type":"string"}},"required":["param1"]})", // JSON schema
            true,                                   // Enabled by default?
            "Example voice command",                // Example for users
            "rcli action my_feature '{...}'"        // CLI example
        },
        action_my_feature
    );
}
4

Call registration in action_registry.cpp

Edit src/actions/action_registry.cpp and add your registration call:
src/actions/action_registry.cpp
#include "actions/my_category_actions.h"

void ActionRegistry::register_defaults() {
    // Existing registrations...
    register_notes_actions(*this);
    register_reminders_actions(*this);
    // ...

    // Add your category
    register_my_category_actions(*this);
}
5

Update CMakeLists.txt

If you created a new .cpp file, add it to CMakeLists.txt:
CMakeLists.txt
add_library(rcli STATIC
    # ...
    src/actions/my_category_actions.cpp
)
6

Rebuild and test

cd build
cmake --build . -j$(sysctl -n hw.ncpu)
./rcli actions           # Should list your new action
./rcli action my_feature '{"param1": "test"}'

Action Helpers

src/actions/action_helpers.h provides utilities for parsing JSON and escaping strings:

JSON Parsing

// Extract string from JSON
std::string value = json_get_string(args_json, "key");

// Extract boolean
bool flag = json_get_bool(args_json, "enabled");

// Extract integer
int count = json_get_int(args_json, "count");

String Escaping

// Escape for AppleScript
std::string safe = escape_applescript(user_input);

// Escape for shell commands
std::string safe = escape_shell(user_input);

Execution

// Run AppleScript
auto result = run_applescript(R"(
    tell application "Finder"
        activate
    end tell
)");

if (result.success) {
    // result.output contains stdout
} else {
    // result.error contains stderr
}

// Run shell command
auto result = run_shell("open -a Safari");

ActionResult Structure

struct ActionResult {
    bool        success;     // Did the action succeed?
    std::string output;      // Human-readable success message
    std::string error;       // Human-readable error message (if !success)
    std::string raw_json;    // Machine-parseable JSON result
};

Success Example

return {
    true,                                    // success
    "Created reminder: Buy milk",            // output
    "",                                       // error (empty)
    "{\"action\": \"create_reminder\", \"title\": \"Buy milk\", \"status\": \"created\"}"
};

Failure Example

return {
    false,                                   // success
    "",                                       // output (empty)
    "Safari is not running",                 // error
    "{\"error\": \"Safari is not running\"}"  // raw_json
};

JSON Schema Format

The LLM uses JSON schemas to understand action parameters. Use this format:
{
  "type": "object",
  "properties": {
    "param1": {"type": "string", "description": "First parameter"},
    "param2": {"type": "number", "description": "Second parameter"},
    "param3": {"type": "boolean", "description": "Optional flag"}
  },
  "required": ["param1"]
}
Escape quotes when embedding in C++ strings:
R"({"type":"object","properties":{"param1":{"type":"string"}},"required":["param1"]})"

Tool Calling Flow

1

User speaks or types

User: "Create a note called Project Ideas"
2

LLM receives tool definitions

The LLM sees all enabled actions as tool definitions:
{
  "name": "create_note",
  "description": "Create a new note in Apple Notes",
  "parameters": {"type":"object","properties":{"title":{"type":"string"}},...}
}
3

LLM generates tool call

The LLM responds with a tool call in its native format:
Qwen3 format
<tool_call>
{"name": "create_note", "arguments": {"title": "Project Ideas"}}
</tool_call>
4

ToolEngine parses and executes

The ToolEngine extracts the call and dispatches to ActionRegistry:
auto result = registry.execute("create_note", '{"title": "Project Ideas"}');
5

Action executes

The action function runs:
run_applescript("tell application \"Notes\" to make new note...");
6

Result returned to LLM

The LLM sees:
{"action": "create_note", "title": "Project Ideas", "status": "created"}
And responds to the user:
RCLI: I've created a note called Project Ideas.

Testing Your Action

# Direct execution
rcli action my_feature '{"param1": "test"}'

# Via LLM
rcli ask "use my feature with test parameter"

Action Categories

Use these categories for consistency:
  • productivity — Notes, reminders, shortcuts
  • communication — Messages, FaceTime
  • media — Spotify, Apple Music, volume
  • system — Dark mode, volume, lock screen, battery, Wi-Fi
  • window — Close, minimize, fullscreen
  • web — Search, YouTube, Maps
  • clipboard — Read/write clipboard
  • files — Search files
  • browser — Get URL, list tabs

Advanced: Native macOS APIs

Some actions use Objective-C APIs instead of AppleScript for better performance. See src/audio/mic_permission.mm for examples.
src/audio/mic_permission.mm
#import <AVFoundation/AVFoundation.h>

bool request_mic_permission() {
    __block bool granted = false;
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    
    [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio
                             completionHandler:^(BOOL granted_) {
        granted = granted_;
        dispatch_semaphore_signal(sem);
    }];
    
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    return granted;
}

Enabling/Disabling Actions

Users can enable/disable actions via:
rcli actions                 # List all actions
# Actions are persisted in ~/Library/RCLI/config/actions.json
Default state is set in ActionDef:
ActionDef{
    "my_feature",
    "Description",
    "category",
    R"({...})",
    true,  // <-- default_enabled = true
    "Example",
    "CLI example"
}

Next Steps

Project Structure

Understand the codebase organization

Contributing

Submit your new action as a PR

Building from Source

Build and test your changes

Build docs developers (and LLMs) love