afwf Introduction¶
afwf is a Python framework for building Alfred Workflows. It covers three things:
A typed API for Script Filter output — build drop-down items and script filter responses as Python objects, not raw dicts.
A uvx-based deployment model — ship your workflow as a PyPI package, invoke it from Alfred with a single
uvxline. No virtualenv to babysit.Optional workflow utilities — fuzzy ranking, disk caching, and other primitives that come up in nearly every workflow.
Quick Example¶
A Script Filter that fuzzy-ranks bookmarks against the user’s query:
from afwf.api import ScriptFilter
from afwf.opt.fuzzy_item.api import Item, FuzzyItemMatcher
BOOKMARKS = [
("Alfred App", "https://www.alfredapp.com/"),
("Python", "https://www.python.org/"),
("GitHub", "https://github.com/"),
("Stack Overflow", "https://stackoverflow.com/"),
]
def search_bookmarks(query: str = "") -> ScriptFilter:
items = [
Item(title=title, subtitle=url, arg=url).set_fuzzy_match_name(title)
for title, url in BOOKMARKS
]
if query.strip():
items = FuzzyItemMatcher.from_items(items).match(query, threshold=0) or items
sf = ScriptFilter()
sf.items.extend(items)
return sf
The Alfred Script field:
~/.local/bin/uvx your-workflow@1.0.0 search-bookmarks --query '{query}'
That’s it. No main.py, no hardcoded interpreter path, no per-machine setup.
The Typed Script Filter API¶
Alfred’s Script Filter speaks JSON. Writing that JSON by hand is fragile — a typo in a key name produces no error, just a silently broken workflow. afwf gives you a proper Python API instead.
Item
from afwf.api import Item
item = (
Item(
title="Alfred Documentation",
subtitle="Open in browser",
arg="https://www.alfredapp.com/help/",
)
.open_url("https://www.alfredapp.com/help/") # Enter → open URL
.add_modifier(mod="cmd", subtitle="Copy URL", arg="https://www.alfredapp.com/help/")
)
Item covers the full Alfred Script Filter JSON spec — title, subtitle, arg, uid, icon, modifiers, variables, and more. Methods are chainable.
ScriptFilter
from afwf.api import ScriptFilter
sf = ScriptFilter()
sf.items.append(item)
sf.to_script_filter() # → dict, useful in unit tests
sf.send_feedback() # → writes JSON to stdout, picked up by Alfred
The uvx Deployment Model¶
The old workflow deployment story is painful:
A virtualenv lives inside the workflow directory, bloating it with tens of MB of packages.
The Python interpreter path is hardcoded in Alfred’s Script field.
That path breaks whenever the venv moves, Python upgrades, or you switch machines.
Every new machine needs the same manual setup.
The uvx model cuts all of this out. Publish your workflow logic to PyPI, then point Alfred at it:
~/.local/bin/uvx your-package@1.0.0 subcommand --query '{query}'
uvx resolves, downloads, and caches the exact version on first run. Nothing to install. Nothing to configure. Upgrading is a one-character change in Alfred’s Script field.
Before |
With uvx |
|---|---|
Hardcoded |
|
Breaks on Python upgrade or venv move |
Self-contained, always works |
Manual setup on every machine |
Zero setup — cached on first run |
Version drift across machines |
Version pinned in the Script field |
See ../12-uvx-Deployment-Model/index for the full walkthrough.
Optional Utilities¶
Fuzzy ranking (afwf.opt.fuzzy_item)
Sort a list of Items by relevance to a query string. Built on rapidfuzz.
from afwf.opt.fuzzy_item.api import Item, FuzzyItemMatcher
items = [Item(title=t).set_fuzzy_match_name(t) for t in ["Alfred", "Python", "GitHub"]]
results = FuzzyItemMatcher.from_items(items).match("pyth", threshold=0)
# → [Item("Python"), ...]
Disk cache (afwf.opt.cache)
Persist expensive results — API responses, filesystem scans — between Alfred invocations. Built on diskcache.
Programmatic Access to Alfred and Your Project¶
afwf also exposes the Alfred runtime environment and your project layout as first-class Python objects,
so scripts, build tools, and tests can navigate paths without hardcoding anything.
AlfredPreferences — the Alfred preferences folder
from afwf.api import AlfredPreferences
prefs = AlfredPreferences()
prefs.dir_alfred_preferences # Path to Alfred.alfredpreferences
prefs.dir_workflows # …/workflows/
# iterate every installed workflow
for wf in prefs.list_workflows():
print(wf.name, wf.bundle_id, wf.version)
# look up one workflow by its UUID
wf = prefs.get_workflow("76458317-5B0A-40E7-A328-DC6C900EC1B9")
AlfredPreferences reads ~/Library/Application Support/Alfred/prefs.json to locate the
active preferences folder (which may live on a sync drive), so it works regardless of where
Alfred is configured to store its data.
AlfredWorkflow — a single workflow directory
from afwf.api import AlfredWorkflow
from pathlib import Path
wf = AlfredWorkflow(dir_workflow=Path("…/workflows/user.workflow.<UUID>"))
wf.workflow_id # UUID string extracted from the folder name
wf.name # human-readable name from info.plist
wf.bundle_id # reverse-DNS bundle id, e.g. "MacHu-GWU.my-workflow"
wf.version # version string, e.g. "1.0.0"
wf.description # short description
wf.created_by # author
wf.web_address # homepage / repo URL
wf.disabled # bool — is the workflow currently disabled?
All attributes are lazy: info.plist is only parsed on first access.
AfwfProject — binds your source repo to its deployed workflow folder
from afwf.api import AlfredPreferences, AfwfProject
from pathlib import Path
prefs = AlfredPreferences()
wf = prefs.get_workflow("76458317-5B0A-40E7-A328-DC6C900EC1B9")
proj = AfwfProject(
dir_project_root=Path("~/my-workflow"),
alfred_workflow=wf,
)
# source-side paths
proj.package_name # from [project] name in pyproject.toml
proj.dir_package # <root>/<package_name>/
proj.path_project_info_plist # info.plist checked into VCS
proj.path_project_icon_png # icon.png checked into VCS
# workflow-side paths
proj.alfred_workflow.dir_workflow # the live Alfred folder
proj.alfred_workflow.path_info_plist
AfwfProject is useful in build scripts that sync info.plist / icon.png between the
Alfred folder and the git repo, or that inspect the deployed version before publishing to PyPI.