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

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

The workflow structure is defined in 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 and the Inject Crate

When a workflow runs, 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, windows.rs, and 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, which handles loading and saving the user's 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:

[[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, 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 and 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 using the persistence logic in 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:

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 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 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.
  • Persistence: Configurations save to ~/.config/openlogi/config.toml via crates/openlogi-core/src/config.rs.
  • Execution: The run_workflow function in the inject crate (macos.rs, windows.rs, 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.

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).

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →