Compile a small Python-shaped DSL into Apple Shortcuts on macOS.
Shoutout to Cherri for inspiration and ideas.
from shortcutpy.dsl import shortcut, ask_for_text, choose_from_menu, show_result
@shortcut(color="yellow", glyph="hand")
def greeting():
name = ask_for_text("What is your name?")
tone = choose_from_menu("Tone", ["formal", "casual"])
if tone == "formal": message = f"Good day, {name}."
else: message = f"Hey {name}!"
show_result(message)With bare @shortcut, the shortcut name defaults from the function name, so greeting becomes Greeting and get_current_weather becomes Get Current Weather.
- one file = one shortcut
- one
@shortcut-decorated function - assignments, action calls,
if/else,for item in items,for _ in range(n), andreturn - literals, f-strings, list literals, dict literals, and
x[...]lookups - native signing via
shortcuts sign
Install in editable mode:
pip install -e .[dev]Compile and sign:
shortcutpy path/to/shortcut.pyBy default this writes <shortcut name>.shortcut next to the source file. For instance, @shortcut(name="Current Weather") writes Current Weather.shortcut.
Compile, sign, and open the result in Shortcuts.app:
shortcutpy -o path/to/shortcut.pyWith -o and no -O, shortcutpy builds into a temp directory first, so it doesn't leave a .shortcut file next to your source.
Write somewhere else:
shortcutpy -O /tmp/my.shortcut path/to/shortcut.pyDump an installed shortcut from Shortcuts.app to the text reference format used in examples/:
shortcutpy dump "timestamp discord" -O examples/timestamp_discord.original.txtWrite an unsigned shortcut file instead:
shortcutpy path/to/shortcut.py --skip-signKeep the intermediate unsigned file when signing:
shortcutpy path/to/shortcut.py --keep-unsignedChoose a signing mode:
shortcutpy path/to/shortcut.py --mode anyoneModule level:
- only imports from
shortcutpy.dsl - one shortcut function per file
- no other top-level statements
Decorator and function shape:
@shortcut@shortcut(...)- any function name
- no function parameters
name=,color=,glyph=, andinput_types=on the decorator- if
name=is omitted, the function name is converted fromsnake_caseto spaced capitalized words
Supported statements:
- simple assignment:
x = ... - action call as a statement
if/elsefor item in itemsfor i in range(<int literal>)return
Supported expressions:
- variable references
- literals:
str,int,float,bool,None - DSL action calls
- list literals
- dict literals, including variable keys like
{key: value} - dictionary lookup with literal or variable keys:
mapping["key"]ormapping[key] - list indexing with non-negative integer literals:
items[0] - simple f-strings with name interpolation only
Supported if conditions:
- simple comparisons:
==,!=,>,>=,<,<= - boolean name checks:
if flag: - negated boolean name checks:
if not flag:
Not currently supported:
- arithmetic like
a + b - boolean
and/or - chained comparisons
while- comprehensions
- attribute access like
obj.attr - slices like
items[1:3] - dynamic list indices like
items[i] - method calls
- destructuring assignment
- starred args or
**kwargs - aliasing DSL calls to different names
See examples/README.md and the files in examples.
shortcutpy.dsl currently provides the hand-written MVP helpers:
shortcutask_for_datetimeask_for_textchoose_from_menushow_resultget_filespreferred_languageresize_imagesave_fileraw_actionshortcut_inputunix_timestamp
It also exposes a generated catalog of Shortcuts actions sourced from Cherri's action definitions, including wrappers like alert, show_notification, toggle_dnd, combine_images, resize_image_by_percent, get_current_weather, and save_file_to_path.
For the exact generated names and typed signatures, see shortcutpy/dsl.pyi.
shortcut_input() references the Shortcut Input variable and flips WFWorkflowHasShortcutInputVariables in the emitted payload.
preferred_language() emits a small macOS shell-script action that returns the current two-letter AppleLocale language code, so using it may trigger Shortcuts' shell-script permission prompt.
input_types= on @shortcut narrows WFWorkflowInputContentItemClasses; for instance input_types=["text", "date"].
glyph= accepts a small built-in name map or an integer glyph id. color= accepts the standard Shortcuts color names or an integer color value.
pip install -e .[dev]For internals, release steps, and the signing/debugging notes, see DEV.md.