# How to Create Multi-Step Workflows for Keyboard Remapping in OpenLogi

> Learn how to create multi-step keyboard remapping workflows in OpenLogi. This guide explains how to store and process sequences of WorkflowSteps asynchronously for powerful customization.

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

---

**OpenLogi executes multi-step keyboard workflows by storing a sequence of `WorkflowStep` variants inside the `Action::Workflow` type and processing them asynchronously through the platform-specific inject layer.**

OpenLogi is an open-source input remapping framework that supports complex automation through sequential action lists. Creating multi-step workflows for keyboard remapping in OpenLogi allows you to chain key presses, delays, text input, and system scripts into a single button activation. This guide explains the architecture, configuration methods, and runtime behavior based on the actual source implementation in the AprilNEA/OpenLogi repository.

## Understanding the Workflow Architecture

A workflow in OpenLogi is essentially a **scripted sequence** that transforms a single physical key press into multiple automated actions. The architecture separates data definition from platform-specific execution.

### Core Data Model in [`action.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/action.rs)

The workflow structure is defined in [`crates/openlogi-core/src/binding/action.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/action.rs). Here, a workflow is represented as `Action::Workflow(Vec<WorkflowStep>)`, where `WorkflowStep` is an enum that defines the available step types:

- `PressKey { usage, usage_page }` – Synthesizes a USB HID key press using hexadecimal usage codes (e.g., `usage: 0x04, usage_page: 0x07` for the 'a' key).
- `Delay(u64)` – Pauses execution for the specified number of milliseconds.
- `TypeText(String)` – Generates a series of key press events for each character in the string.
- `RunAppleScript(String)` – Executes the provided AppleScript on macOS (ignored on other platforms).

This model allows workflows to be serialized to TOML and deserialized back into Rust structs at runtime.

### Execution Pipeline in [`effect.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/effect.rs) and the Inject Crate

When a workflow runs, [`crates/openlogi-core/src/binding/effect.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/effect.rs) dispatches each `WorkflowStep` to the appropriate handler. The heavy lifting occurs in [`crates/openlogi-inject/src/inject/macos.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-inject/src/inject/macos.rs), [`windows.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/windows.rs), and [`linux.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/linux.rs), where platform-specific APIs translate the abstract steps into real input events:

- **macOS**: Uses `CGEventTap` to post keyboard events via `hid::press_key`.
- **Windows**: Uses the Win32 `SendInput` API.
- **Linux**: Uses `uinput` to inject events into the kernel.

The desktop UI persists these definitions to [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs), which handles loading and saving the user's [`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml).

## Creating Workflows in Practice

You can define workflows through three primary methods: manual TOML editing, the graphical Actions Ring editor, or programmatic construction using the Rust API.

### Method 1: Manual TOML Configuration

For power users, workflows can be authored directly in `~/.config/openlogi/config.toml`. The `action` field uses the `Workflow` variant, which contains an array of step tables:

```toml
[[bindings]]
button = "F5"
action = { Workflow = [
  { PressKey = { usage = 0x04, usage_page = 0x07 } },
  { Delay = 200 },
  { TypeText = "Hello World!" },
  { RunAppleScript = "display notification \"Done\"" }
] }

```

Each step is a table keyed by the step type name. The TOML schema mirrors the `WorkflowStep` enum definition in [`action.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/action.rs), ensuring type-safe deserialization at startup.

### Method 2: Using the Desktop UI Editor

The recommended approach for most users is the **Actions Ring** interface implemented in [`crates/openlogi-desktop/src/features/keyboard/editors.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/features/keyboard/editors.rs) and [`crates/openlogi-desktop/src/ui/action.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/ui/action.rs):

1. Open the Actions Ring (default shortcut: ⌘ Space).
2. Click the **gear** icon on the target key and select **Edit Action**.
3. Choose **Workflow** from the action type list.
4. Click **Add Step** to insert `PressKey`, `Delay`, `TypeText`, or `RunAppleScript` steps.
5. Configure parameters, reorder steps via drag-and-drop, and click **Save**.

The UI automatically serializes the workflow to [`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml) using the persistence logic in [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs).

### Method 3: Programmatic Rust API

If you are developing a plugin or internal tool, construct workflows directly using the `openlogi_core` crate:

```rust
use openlogi_core::binding::{Action, WorkflowStep, Binding};

let workflow = Action::Workflow(vec![
    WorkflowStep::PressKey { usage: 0x04, usage_page: 0x07 },
    WorkflowStep::Delay(200),
    WorkflowStep::TypeText("Hello World!".into()),
    WorkflowStep::RunAppleScript(r#"display notification "Done""#.into()),
]);

let binding = Binding {
    button: "F5".into(),
    action: workflow,
    ..Default::default()
};

```

This creates the same data structure that the TOML parser and UI editor produce, allowing you to bind workflows programmatically at compile time or runtime.

## How Workflows Execute at Runtime

Understanding the execution flow helps debug timing issues and platform-specific behavior:

1. **HID++ Event Capture**: When the user presses a bound key, the **agent** receives a HID++ event from the hardware.
2. **IPC Transmission**: The event is forwarded via **tarpc IPC** to the **desktop** process.
3. **Action Resolution**: The desktop process looks up the bound `Action`. If it finds `Action::Workflow`, it creates a `Script::Workflow` and hands it to the **inject** crate.
4. **Asynchronous Execution**: `inject::run_workflow` spawns a worker thread via `run_workflow_async` to prevent UI blocking.
5. **Step Processing**: The worker iterates through each `WorkflowStep`:
   - `PressKey` calls `hid::press_key` using the platform-specific backend (`CGEventTap`, `SendInput`, or `uinput`).
   - `Delay` calls the system sleep function for the specified duration.
   - `TypeText` splits the string into individual `PressKey` events.
   - `RunAppleScript` spawns `osascript` on macOS; on Windows and Linux, the step is skipped with a `tracing::warn!` log.

## Debugging Multi-Step Workflows

When workflows behave unexpectedly, use these diagnostic strategies:

- **Enable tracing logs**: The inject layer emits `tracing::info!("workflow step: …")` logs for each executed step. Set `RUST_LOG=info` to view the sequence.
- **Check platform warnings**: Steps like `RunAppleScript` and `TypeText` emit `tracing::warn!` messages when executed on unsupported platforms (non-macOS).
- **Validate TOML syntax**: Ensure [`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml) uses correct table keys matching the `WorkflowStep` variant names (e.g., `PressKey` not `press_key`).
- **Inspect step order**: Use the desktop UI in [`crates/openlogi-desktop/src/ui/action.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/ui/action.rs) to visually confirm the step sequence before saving.

## Summary

- **Data Definition**: Workflows are `Vec<WorkflowStep>` stored in `Action::Workflow`, defined in [`crates/openlogi-core/src/binding/action.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/action.rs).
- **Persistence**: Configurations save to `~/.config/openlogi/config.toml` via [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs).
- **Execution**: The `run_workflow` function in the inject crate ([`macos.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/macos.rs), [`windows.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/windows.rs), [`linux.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/linux.rs)) processes steps asynchronously using platform-native APIs.
- **Authoring**: Use TOML for version-controlled setups, the Actions Ring UI for visual editing, or the Rust API for programmatic generation.
- **Platform Limits**: AppleScript support is macOS-only; other platforms skip these steps with warnings.

## Frequently Asked Questions

### What file format does OpenLogi use to store multi-step workflows?

OpenLogi stores workflow definitions in a TOML configuration file located at `~/.config/openlogi/config.toml`. Each workflow is serialized as an `Action::Workflow` table containing an array of step objects, with persistence logic handled in [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs).

### Can I use AppleScript workflows on Windows or Linux?

No. The `RunAppleScript` step is only functional on macOS where it spawns the `osascript` binary. On Windows and Linux, this step is ignored and emits a `tracing::warn!` log entry. Cross-platform workflows should avoid platform-specific script steps or conditionalize them based on the target OS.

### How does OpenLogi prevent the UI from freezing during workflow execution?

The inject crate spawns a dedicated worker thread via `run_workflow_async` before iterating through the `WorkflowStep` vector. This asynchronous execution model ensures that long delays or complex typing sequences do not block the main desktop UI thread or the tarpc IPC handler.

### What are the `usage` and `usage_page` values in `PressKey` steps?

These are USB HID usage codes defined in the USB specification. For example, `usage: 0x04` with `usage_page: 0x07` represents the 'a' key on a standard keyboard. These hexadecimal values map directly to the HID scancodes that `hid::press_key` transmits via `CGEventTap` (macOS), `SendInput` (Windows), or `uinput` (Linux).