# How to Implement a Custom Action in the OpenLogi Action Catalog: A Complete Guide

> Learn to implement a custom action in the OpenLogi Action Catalog. This guide covers adding variants, updating UI metadata, defining icons, and implementing execution logic for your custom OpenLogi actions.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: how-to-guide
- Published: 2026-09-13

---

**To implement a custom action in OpenLogi, add a new variant to the `Action` enum in `openlogi-core`, update the UI metadata in the `impl Action` block, optionally define an icon in `ActionRingIcon`, and implement the OS-level execution logic in `openlogi-inject`.**

OpenLogi is a Rust-based automation platform developed by AprilNEA that enables users to trigger workflows through an Actions Ring interface. The project organizes its architecture into multiple crates, with the core action definitions residing in `openlogi-core` and runtime execution handled by `openlogi-inject`. When you implement a custom action in the OpenLogi action catalog, you are extending the central `Action` enum while maintaining serialization stability and cross-platform compatibility.

## Step-by-Step Implementation Guide

### 1. Define the Action Enum Variant in openlogi-core

All action types are defined in [`crates/openlogi-core/src/binding/action.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/action.rs). The `Action` enum uses explicit payload types to maintain a stable TOML serialization schema on disk.

Locate the `Action` enum definition and add your new variant with an appropriate payload type. For actions requiring string input (such as URLs or text), use `String` or a dedicated struct:

```rust
// crates/openlogi-core/src/binding/action.rs
pub enum Action {
    // ... existing variants ...
    /// Open a user-specified URL in the default browser.
    OpenUrl(String),
    // ... other variants ...
}

```

The variant name is serialized verbatim, ensuring configuration files remain compatible across versions. This "stability contract" is documented in the file comments.

### 2. Add UI Metadata and Translation Keys

The `impl Action` block (approximately line 300 in the same file) defines how the action appears in the user interface. You must provide three metadata methods for your new variant:

```rust
// crates/openlogi-core/src/binding/action.rs
impl Action {
    pub fn label(&self) -> &'static str {
        match self {
            // ... other arms ...
            Self::OpenUrl(_) => "Open URL",
        }
    }
    
    pub fn translation_key(&self) -> &'static str {
        match self {
            // ... other arms ...
            Self::OpenUrl(_) => "actions.open_url",
        }
    }
    
    pub fn category(&self) -> Category {
        match self {
            // ... other arms ...
            Self::OpenUrl(_) => Category::System,
        }
    }
}

```

**Note:** Do not add payload-carrying variants to the `for_each_unit_action!` macro. This macro only lists payload-free actions that appear directly in the UI picker without additional configuration.

### 3. Register an Icon for the Actions Ring

To display a custom glyph in the Actions Ring, extend the `ActionRingIcon` enum in [`crates/openlogi-core/src/binding/action_ring/icon.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/action_ring/icon.rs):

```rust
// crates/openlogi-core/src/binding/action_ring/icon.rs
pub enum ActionRingIcon {
    // ... existing icons ...
    OpenUrl,
}

impl ActionRingIcon {
    pub const fn asset_path(self) -> &'static str {
        match self {
            // ... existing mappings ...
            Self::OpenUrl => "action-icons/globe.svg",
        }
    }
}

```

Ensure you also update `ActionRingIcon::ALL` so the editor can display your new icon in the configuration interface. Place the corresponding SVG asset in the specified path.

### 4. Implement OS-Level Execution Logic

The actual system-level behavior is implemented in the `openlogi-inject` crate. Open [`crates/openlogi-inject/src/lib.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-inject/src/lib.rs) and add a match arm in the `execute` function to handle your new action:

```rust
// crates/openlogi-inject/src/lib.rs
match action {
    // ... other actions ...
    Action::OpenUrl(url) => {
        #[cfg(target_os = "macos")]
        std::process::Command::new("open")
            .arg(url)
            .spawn()
            .ok();
        #[cfg(target_os = "linux")]
        std::process::Command::new("xdg-open")
            .arg(url)
            .spawn()
            .ok();
        #[cfg(target_os = "windows")]
        std::process::Command::new("cmd")
            .args(&["/C", "start", "", url])
            .spawn()
            .ok();
    }
    // ... other actions ...
}

```

This pattern ensures cross-platform compatibility by using native commands: `open` for macOS, `xdg-open` for Linux, and `cmd /C start` for Windows.

### 5. Expose the Action in the UI Picker (Optional)

By default, payload-carrying actions do not appear in `Action::catalog()`, which powers the UI picker. If you want users to select this action from the pop-over interface:

- Add a template row to the `for_each_unit_action!` macro with the `not_pickable` tag removed
- Supply a default payload or dialog prompt in the UI code to collect the required data (e.g., a text input for the URL)

Otherwise, the action remains available only through manual configuration files or scripting interfaces.

### 6. Update Tests and Validate Changes

Maintain code quality by updating the test suite:

1. **Serialization tests:** Add a unit test in [`crates/openlogi-core/src/binding/action.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/action.rs) verifying the new variant round-trips correctly through serialization.

2. **Catalog parity:** If your action appears in the UI picker, update [`crates/openlogi-desktop/tests/catalog_parity.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/tests/catalog_parity.rs) to ensure every pickable action has a corresponding translation key.

Run the workspace validation commands:

```bash
cargo fmt --check
cargo clippy -p openlogi-core -- -D warnings
cargo test -p openlogi-core

```

These checks enforce the project's linting standards and verify that your changes do not break existing functionality.

## Key Source Files Reference

| File | Role |
|------|------|
| [`crates/openlogi-core/src/binding/action.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/action.rs) | Defines the `Action` enum and core metadata methods (`label`, `translation_key`, `category`). |
| [`crates/openlogi-core/src/binding/action_ring/icon.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/action_ring/icon.rs) | Maps actions to visual icons via the `ActionRingIcon` enum and `asset_path` method. |
| [`crates/openlogi-inject/src/lib.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-inject/src/lib.rs) | Executes actions at the OS level; contains the `execute` function with platform-specific logic. |
| [`crates/openlogi-desktop/tests/catalog_parity.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/tests/catalog_parity.rs) | Ensures UI pickable actions match localization files. |

## Summary

- **Define** your action by adding a variant to the `Action` enum in `openlogi-core` with a stable payload type.
- **Metadata** methods control how the action appears in the UI, including labels and translation keys.
- **Icons** are registered separately in `ActionRingIcon` and mapped to SVG assets for the Actions Ring.
- **Execution** logic belongs in `openlogi-inject` using conditional compilation for cross-platform support.
- **Visibility** in the UI picker requires opt-in via the X-macro table if the action carries no payload.
- **Testing** includes serialization round-trips and catalog parity checks to maintain system integrity.

## Frequently Asked Questions

### What is the difference between payload-carrying and unit actions in OpenLogi?

**Unit actions** are payload-free variants listed in the `for_each_unit_action!` macro that appear directly in the UI picker. **Payload-carrying actions** (like `OpenUrl(String)` or `RunShellCommand(String)`) require additional data and do not appear in the picker unless explicitly templated. This distinction preserves type safety while allowing flexible extensibility.

### Where does the actual execution of an action happen in OpenLogi?

The runtime execution occurs in [`crates/openlogi-inject/src/lib.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-inject/src/lib.rs) within the `execute` function. This crate bridges the abstract `Action` enum with OS-specific system calls, handling platform differences through conditional compilation attributes like `#[cfg(target_os = "macos")]`.

### How do I ensure my custom action appears in the OpenLogi UI picker?

Payload-free actions automatically appear in the picker via the `for_each_unit_action!` macro. For payload-carrying actions, you must either provide a template with default values in the macro (removing the `not_pickable` tag) or implement a UI dialog that collects the required data before instantiating the action.

### What testing is required when adding a new action to OpenLogi?

You must verify serialization stability with a round-trip unit test in the core crate and, if applicable, update the catalog parity test in the desktop crate to validate localization coverage. Always run `cargo clippy` and `cargo test` on the affected packages before submitting changes.