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: 0x07for 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
CGEventTapto post keyboard events viahid::press_key. - Windows: Uses the Win32
SendInputAPI. - Linux: Uses
uinputto 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:
- Open the Actions Ring (default shortcut: ⌘ Space).
- Click the gear icon on the target key and select Edit Action.
- Choose Workflow from the action type list.
- Click Add Step to insert
PressKey,Delay,TypeText, orRunAppleScriptsteps. - 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:
- HID++ Event Capture: When the user presses a bound key, the agent receives a HID++ event from the hardware.
- IPC Transmission: The event is forwarded via tarpc IPC to the desktop process.
- Action Resolution: The desktop process looks up the bound
Action. If it findsAction::Workflow, it creates aScript::Workflowand hands it to the inject crate. - Asynchronous Execution:
inject::run_workflowspawns a worker thread viarun_workflow_asyncto prevent UI blocking. - Step Processing: The worker iterates through each
WorkflowStep:PressKeycallshid::press_keyusing the platform-specific backend (CGEventTap,SendInput, oruinput).Delaycalls the system sleep function for the specified duration.TypeTextsplits the string into individualPressKeyevents.RunAppleScriptspawnsosascripton macOS; on Windows and Linux, the step is skipped with atracing::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. SetRUST_LOG=infoto view the sequence. - Check platform warnings: Steps like
RunAppleScriptandTypeTextemittracing::warn!messages when executed on unsupported platforms (non-macOS). - Validate TOML syntax: Ensure
config.tomluses correct table keys matching theWorkflowStepvariant names (e.g.,PressKeynotpress_key). - Inspect step order: Use the desktop UI in
crates/openlogi-desktop/src/ui/action.rsto visually confirm the step sequence before saving.
Summary
- Data Definition: Workflows are
Vec<WorkflowStep>stored inAction::Workflow, defined incrates/openlogi-core/src/binding/action.rs. - Persistence: Configurations save to
~/.config/openlogi/config.tomlviacrates/openlogi-core/src/config.rs. - Execution: The
run_workflowfunction 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →