Creating Plugins
This guide walks you through creating your first Freeway plugin from scratch.
Quick Start
Section titled “Quick Start”1. Create the Plugin Folder
Section titled “1. Create the Plugin Folder”Create a new folder in your Plugins directory:
mkdir -p ~/Library/Application Support/Freeway/Plugins/my-first-plugincd ~/Library/Application Support/Freeway/Plugins/my-first-plugin2. Create meta.json
Section titled “2. Create meta.json”Create meta.json with your plugin’s metadata:
{ "name": "My First Plugin", "description": "Converts transcribed text to uppercase", "hooks": ["before_paste"], "dependencies": []}3. Create plugin.py
Section titled “3. Create plugin.py”Create plugin.py with your plugin logic:
import freeway
def before_paste(): text = freeway.get_text() freeway.set_text(text.upper()) freeway.log("Converted text to uppercase")4. Enable the Plugin
Section titled “4. Enable the Plugin”- Open Freeway Preferences → Plugins
- Find “My First Plugin” in the list
- Toggle it on
That’s it! Now when you dictate, your text will be converted to uppercase.
meta.json Reference
Section titled “meta.json Reference”Required Fields
Section titled “Required Fields”{ "name": "Plugin Name", "hooks": ["before_paste"]}All Fields
Section titled “All Fields”{ "name": "My Plugin", "description": "What the plugin does", "instructions": "Detailed setup instructions shown in the plugin settings", "hooks": ["before_paste"], "dependencies": ["requests", "openai"], "trigger": { "pattern": "hey assistant", "matchType": "startsWith" }, "author": { "name": "Your Name", "url": "https://yourwebsite.com", "plugin_page": "https://yourwebsite.com/plugin-docs", }, "settings": []}Hooks Array
Section titled “Hooks Array”Specify which pipeline events your plugin responds to:
{ "hooks": ["before_recording", "before_paste", "after_paste"]}Your plugin.py must have a function matching each hook name:
def before_recording(): pass
def before_paste(): pass
def after_paste(): passDependencies
Section titled “Dependencies”List any Python packages your plugin needs:
{ "dependencies": ["requests", "openai", "beautifulsoup4"]}Freeway creates a virtual environment and installs these automatically.
Trigger
Section titled “Trigger”Define when your plugin should run based on the transcribed text:
{ "trigger": { "pattern": "hey freeway", "matchType": "startsWith" }}Match types:
| Type | Description |
|---|---|
startsWith | Text must start with the pattern |
endsWith | Text must end with the pattern |
match | Text must exactly match the pattern (case-insensitive) |
contains | Text must contain the pattern anywhere (default) |
regex | Pattern is treated as a regular expression |
Regex examples:
{ "trigger": { "pattern": "^(hey|ok|hi) freeway", "matchType": "regex" }}{ "trigger": { "pattern": "^search (for |on )?", "matchType": "regex" }}Notes:
- If no trigger is specified, the plugin runs on every transcription
- Text is normalized before matching: punctuation is removed and whitespace is trimmed
- Matching is case-insensitive
- Users can override trigger settings in the plugin preferences
- Use
freeway.get_trigger()in your plugin to access the trigger configuration
Settings Schema
Section titled “Settings Schema”Make your plugin configurable with a settings UI.
Input Field (Text)
Section titled “Input Field (Text)”{ "type": "input", "name": "api_key", "label": "API Key", "placeholder": "Enter your key", "required": true, "help_text": "Get your key from the website", "rows": 1}Set rows > 1 for a multiline text area:
{ "type": "input", "name": "system_prompt", "label": "System Prompt", "rows": 5}Toggle (Boolean)
Section titled “Toggle (Boolean)”{ "type": "toggle", "name": "enabled", "label": "Enable feature", "default": true}Select (Dropdown)
Section titled “Select (Dropdown)”{ "type": "select", "name": "model", "label": "Model", "default": "gpt-4o", "options": [ {"value": "gpt-4o", "label": "GPT-4o"}, {"value": "gpt-4o-mini", "label": "GPT-4o Mini"} ]}Radio (Button Group)
Section titled “Radio (Button Group)”{ "type": "radio", "name": "format", "label": "Output Format", "options": [ {"value": "plain", "label": "Plain Text"}, {"value": "markdown", "label": "Markdown"} ]}Text (Static Info)
Section titled “Text (Static Info)”Display static text or instructions:
{ "type": "text", "text": "👉 Get your API key at https://example.com/api"}File Picker
Section titled “File Picker”{ "type": "file", "name": "config_file", "label": "Configuration File", "help_text": "Select a JSON config file"}Folder Picker
Section titled “Folder Picker”{ "type": "folder", "name": "output_dir", "label": "Output Directory"}Accessing Settings
Section titled “Accessing Settings”In your plugin.py, use the freeway SDK to access user settings:
import freeway
def before_paste(): # Get a single setting api_key = freeway.get_setting("api_key")
# Get all settings settings = freeway.get_settings()
if not api_key: freeway.log("No API key configured") return
# Use the settings...Recording Metadata
Section titled “Recording Metadata”Access information about the recording session via the metadata file:
import freewayimport json
def before_paste(): meta_path = freeway.get_meta_path()
with open(meta_path) as f: meta = json.load(f)
print(meta)The metadata includes:
{ "id": "7CBE9007-EBBA-493E-8D64-B49F00883161", "app_version": "1.2.3", "recorded_at": "2025-12-15T20:15:18.930Z", "recorded_at_timestamp": 1765829718.93039, "audio_duration": 4.18, "sample_rate": 16000, "microphone_id": "AppleUSBAudioEngine:...", "microphone_name": "Studio Display Microphone", "text": "Hello, this is a test transcription.", "word_count": 6}Error Handling
Section titled “Error Handling”Always handle errors gracefully to avoid breaking the transcription pipeline:
import freeway
def before_paste(): try: text = freeway.get_text() # Your processing logic... freeway.set_text(processed_text) except Exception as e: freeway.log(f"Error: {str(e)}") # Don't modify text if there's an error # The original text will be pastedWorking with Triggers
Section titled “Working with Triggers”When your plugin uses a trigger pattern, you can access the trigger configuration using freeway.get_trigger():
import freeway
def before_paste(): trigger = freeway.get_trigger() text = freeway.get_text()
if trigger and trigger.get("pattern"): pattern = trigger["pattern"] match_type = trigger.get("match_type", "contains")
freeway.log(f"Triggered by: {pattern} ({match_type})")
# Strip trigger phrase from start of text if match_type == "startsWith": text_lower = text.lower() pattern_lower = pattern.lower() if text_lower.startswith(pattern_lower): text = text[len(pattern):].strip() freeway.set_text(text)Best Practices
Section titled “Best Practices”- Always use virtual environments — Freeway creates one automatically
- Handle errors gracefully — Don’t crash the transcription pipeline
- Keep hooks fast — Long-running tasks delay the paste
- Use status text — Show progress for slow operations
- Log for debugging — Use
freeway.log()to track issues - Validate settings — Check for required settings before using them
- Use triggers for voice commands — Define trigger patterns to make your plugin respond to specific phrases
Distributing Your Plugin
Section titled “Distributing Your Plugin”Create a ZIP File
Section titled “Create a ZIP File”cd ~/Library/Application Support/Freeway/Pluginszip -r my-plugin.zip my-plugin/ -x "my-plugin/venv/*" -x "my-plugin/settings.json"Exclude:
venv/— Will be recreated on installsettings.json— User-specific settings
Share Your Plugin
Section titled “Share Your Plugin”- Upload to GitHub
- Share on forums/communities
- Submit to Freeway’s plugin directory (coming soon)