Skip to content

Creating Plugins

This guide walks you through creating your first Freeway plugin from scratch.


Create a new folder in your Plugins directory:

Terminal window
mkdir -p ~/Library/Application Support/Freeway/Plugins/my-first-plugin
cd ~/Library/Application Support/Freeway/Plugins/my-first-plugin

Create meta.json with your plugin’s metadata:

{
"name": "My First Plugin",
"description": "Converts transcribed text to uppercase",
"hooks": ["before_paste"],
"dependencies": []
}

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")
  1. Open Freeway Preferences → Plugins
  2. Find “My First Plugin” in the list
  3. Toggle it on

That’s it! Now when you dictate, your text will be converted to uppercase.


{
"name": "Plugin Name",
"hooks": ["before_paste"]
}
{
"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",
"email": "[email protected]"
},
"settings": []
}

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():
pass

List any Python packages your plugin needs:

{
"dependencies": ["requests", "openai", "beautifulsoup4"]
}

Freeway creates a virtual environment and installs these automatically.

Define when your plugin should run based on the transcribed text:

{
"trigger": {
"pattern": "hey freeway",
"matchType": "startsWith"
}
}

Match types:

TypeDescription
startsWithText must start with the pattern
endsWithText must end with the pattern
matchText must exactly match the pattern (case-insensitive)
containsText must contain the pattern anywhere (default)
regexPattern 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

Make your plugin configurable with a settings UI.

{
"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
}
{
"type": "toggle",
"name": "enabled",
"label": "Enable feature",
"default": true
}
{
"type": "select",
"name": "model",
"label": "Model",
"default": "gpt-4o",
"options": [
{"value": "gpt-4o", "label": "GPT-4o"},
{"value": "gpt-4o-mini", "label": "GPT-4o Mini"}
]
}
{
"type": "radio",
"name": "format",
"label": "Output Format",
"options": [
{"value": "plain", "label": "Plain Text"},
{"value": "markdown", "label": "Markdown"}
]
}

Display static text or instructions:

{
"type": "text",
"text": "👉 Get your API key at https://example.com/api"
}
{
"type": "file",
"name": "config_file",
"label": "Configuration File",
"help_text": "Select a JSON config file"
}
{
"type": "folder",
"name": "output_dir",
"label": "Output Directory"
}

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

Access information about the recording session via the metadata file:

import freeway
import 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
}

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 pasted

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)

  1. Always use virtual environments — Freeway creates one automatically
  2. Handle errors gracefully — Don’t crash the transcription pipeline
  3. Keep hooks fast — Long-running tasks delay the paste
  4. Use status text — Show progress for slow operations
  5. Log for debugging — Use freeway.log() to track issues
  6. Validate settings — Check for required settings before using them
  7. Use triggers for voice commands — Define trigger patterns to make your plugin respond to specific phrases

Terminal window
cd ~/Library/Application Support/Freeway/Plugins
zip -r my-plugin.zip my-plugin/ -x "my-plugin/venv/*" -x "my-plugin/settings.json"

Exclude:

  • venv/ — Will be recreated on install
  • settings.json — User-specific settings
  • Upload to GitHub
  • Share on forums/communities
  • Submit to Freeway’s plugin directory (coming soon)