.. _Example-Workflow-Architecture: The afwf Example Workflow: Architecture and Development Model ============================================================================== The ``afwf`` library ships a complete, runnable Alfred workflow that demonstrates every pattern covered in the next four documents. This document explains the architecture of that example workflow and the development model behind it. Install the workflow from the repo root to follow along interactively: .. code-block:: text afwf-dev.alfredworkflow ← double-click to install in Alfred The source of truth is the plain-text plist file alongside it: .. code-block:: text info.plist ← human-readable XML; captures everything the Alfred UI shows The Three-Layer Development Model ------------------------------------------------------------------------------ ``afwf``-based workflows are built in three independent layers: .. code-block:: text ┌─────────────────────────────────────────────────────────┐ │ Layer 1 — Python logic │ │ afwf/examples/*.py │ │ Pure functions; no Alfred dependency. │ │ Unit-tested in isolation. │ ├─────────────────────────────────────────────────────────┤ │ Layer 2 — CLI entry point │ │ afwf/examples/cli.py → afwf-examples │ │ fire.Fire(Command) exposes each main() as a subcommand │ │ Alfred calls this binary from its Script field. │ ├─────────────────────────────────────────────────────────┤ │ Layer 3 — Alfred workflow config │ │ info.plist │ │ Declares keywords, Script Filter nodes, Conditional │ │ branches, and action widgets. Static; never changes │ │ when Python logic changes. │ └─────────────────────────────────────────────────────────┘ This separation means: - Python logic is testable with plain ``pytest`` — no Alfred needed. - Adding a new feature only changes Python code; the workflow graph is untouched. - Deploying a new version is a version bump in PyPI; Alfred users upgrade via ``uvx``. The CLI Entry Point ------------------------------------------------------------------------------ ``pyproject.toml`` declares ``afwf-examples`` as a console script entry point: .. code-block:: toml [project.scripts] afwf-examples = "afwf.examples.cli:main" ``afwf/examples/cli.py`` wraps every Python ``main()`` function as a ``fire.Fire`` subcommand: .. literalinclude:: ../../../../afwf/examples/cli.py :language: python Each subcommand maps directly to one Script Filter keyword in Alfred. The ``*-request`` subcommands (``write-file-request``, ``set-settings-request``) are *not* Script Filter endpoints — they are CLI-only entry points called by Alfred's *Run Script* widget (see :doc:`../10-Pattern-Write-Actions/index`). Two Invocation Modes ------------------------------------------------------------------------------ The ``script`` field in each Script Filter node has two forms depending on the deployment context: **Dev / local** — uses the project's virtual environment directly. This is what ``info.plist`` contains in the repo: .. code-block:: bash /Users/sanhehu/Documents/GitHub/afwf-project/.venv/bin/afwf-examples \ search-bookmarks --query '{query}' **Production** — calls the package via ``uvx``, which downloads, caches, and runs the pinned version without any pre-installed virtualenv: .. code-block:: bash ~/.local/bin/uvx --from afwf==1.0.1 \ afwf-examples search-bookmarks --query '{query}' The only thing that changes between the two modes is the binary path. The subcommand name and arguments are identical. info.plist Structure ------------------------------------------------------------------------------ Alfred stores the entire workflow definition in a single plist file. Understanding its structure makes it possible to inspect or edit the workflow without opening Alfred's GUI (where screenshots lose detail). **Top-level keys** .. list-table:: :header-rows: 1 :widths: 25 75 * - Key - Content * - ``objects`` - Array of all nodes (Script Filters, Conditionals, action widgets) * - ``connections`` - Dict mapping source node UID → list of wired destination UIDs * - ``uidata`` - Dict mapping node UID → ``{xpos, ypos}`` (visual canvas positions only) * - ``name`` - Workflow display name (``"afwf dev"``) * - ``bundleid`` - Unique reverse-DNS identifier (``"MacHu-GWU.afwf-dev"``) **A Script Filter node** Each entry in ``objects`` with ``type = alfred.workflow.input.scriptfilter`` has a ``config`` dict. The fields that matter most: .. code-block:: xml keyword afwf-search-bookmarks script .venv/bin/afwf-examples search-bookmarks --query '{query}' argumenttype 1 withspace alfredfiltersresults **Connections** The ``connections`` dict is keyed by the *source* node's UID. Each value is an array of destination objects. A ``sourceoutputuid`` of ``4D36294E-9A14-49FB-A0F4-62E21227E74D`` in a Conditional node means "the matching (``y``) branch output": .. code-block:: xml 2219C90E-2659-4B5A-BDB1-B1A0375C5501 destinationuid 2FE6CFA4-74F5-41A7-A2CA-43A4D999A92D sourceoutputuid 4D36294E-9A14-49FB-A0F4-62E21227E74D The Shared Downstream Widget Pattern ------------------------------------------------------------------------------ The example workflow has six Script Filter triggers but only a small set of Conditional + action widget pairs, shared across triggers: .. code-block:: text afwf-search-bookmarks ──┐ ├──► Conditional (open_url=y) ──► Open URL ├──► Conditional (open_file=y) ──► Open File ◄──── afwf-open-file └──► Conditional (_open_log_file=y) ──► Open File (log) afwf-write-file ────────┐ afwf-set-settings ──────┴──► Conditional (run_script=y) ──► Run Script afwf-memoize (read-only; no downstream action widget) afwf-read-file (read-only; no downstream action widget) afwf-view-settings (read-only; no downstream action widget) Multiple Script Filter triggers wire to the *same* Conditional node. Alfred evaluates the variables on the selected item and routes correctly regardless of which trigger produced it. This keeps the workflow graph small and avoids duplicating widget configuration. Adding a new action type requires adding one Conditional branch and one action widget — once — and then any number of Script Filters can use it immediately by setting the appropriate variable pair on their items.