| name | apple-shortcuts | ||||
|---|---|---|---|---|---|
| description | Read, decompile, create, and edit macOS/iOS Shortcuts programmatically. Use this skill when: (1) User asks about Shortcuts app automation, (2) Reading/inspecting existing shortcut actions, (3) Creating new shortcuts, (4) Editing or modifying shortcut workflows, (5) Exporting shortcuts to files, (6) User mentions "shortcut", "Shortcuts app", "Siri shortcut", or "workflow automation". | ||||
| metadata |
|
- Full Disk Access must be enabled for the terminal app (System Settings → Privacy & Security → Full Disk Access)
- Without FDA,
~/Library/Shortcuts/Shortcuts.sqliteis TCC-protected and inaccessible - Python 3 with
plistlib(built-in) andsqlite3(built-in) — no extra packages needed shortcutsCLI (built into macOS) — for signing and listing
~/Library/Shortcuts/Shortcuts.sqlite
| Table | Purpose |
|---|---|
ZSHORTCUT |
Shortcut metadata (name, ID, action count, dates) |
ZSHORTCUTACTIONS |
Serialized workflow actions (binary plist in ZDATA blob) |
ZSHORTCUTICON |
Icon data (columns: ZGLYPHNUMBER, ZBACKGROUNDCOLORVALUE, ZWORKFLOW) |
ZTRIGGER |
Automation triggers |
ZSHORTCUT: Z_PK, ZNAME, ZACTIONCOUNT, ZWORKFLOWID (UUID), ZACTIONS (FK → ZSHORTCUTACTIONS.Z_PK), ZICON (FK → ZSHORTCUTICON.Z_PK), ZCREATIONDATE, ZMODIFICATIONDATE, ZRUNEVENTSCOUNT, ZLASTRUNEVENTDATE
ZSHORTCUTACTIONS: Z_PK, ZSHORTCUT (FK back), ZDATA (binary plist blob containing the action array)
ZSHORTCUTICON: Z_PK, ZGLYPHNUMBER (SF Symbol glyph ID), ZBACKGROUNDCOLORVALUE (color int), ZWORKFLOW (FK back)
The database has CoreData triggers that call NSCoreDataDATrigger* functions. When writing to the DB from Python, register dummy functions:
db.create_function("NSCoreDataDATriggerInsertUpdatedAffectedObjectValue", 5, lambda *args: None)
db.create_function("NSCoreDataDATriggerUpdatedAffectedObjectValue", 5, lambda *args: None)
db.create_function("NSCoreDataDATriggerDeletedAffectedObjectValue", 5, lambda *args: None)Dates are stored as seconds since 2001-01-01 (CoreData epoch), not Unix epoch. Convert:
import time
now_coredata = time.time() - 978307200 # Unix → CoreData-- CoreData → human-readable in SQLite
datetime(ZMODIFICATIONDATE + 978307200, 'unixepoch')shortcuts list
# Or via SQLite with usage stats:
sqlite3 -readonly ~/Library/Shortcuts/Shortcuts.sqlite "
SELECT ZNAME, ZACTIONCOUNT, ZRUNEVENTSCOUNT,
datetime(ZLASTRUNEVENTDATE + 978307200, 'unixepoch') as last_run
FROM ZSHORTCUT ORDER BY ZNAME;"import sqlite3, plistlib, json
class PlistEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, bytes): return f"<binary {len(obj)} bytes>"
if isinstance(obj, plistlib.UID): return int(obj)
return super().default(obj)
db = sqlite3.connect(
"file:///PATH/TO/Library/Shortcuts/Shortcuts.sqlite?mode=ro", uri=True
)
cur = db.cursor()
cur.execute("""
SELECT s.ZNAME, s.ZACTIONCOUNT, sa.ZDATA
FROM ZSHORTCUT s
JOIN ZSHORTCUTACTIONS sa ON s.ZACTIONS = sa.Z_PK
WHERE s.ZNAME = ?
""", ("Shortcut Name",))
name, count, data = cur.fetchone()
actions = plistlib.loads(data) # Returns a list of action dicts
print(json.dumps(actions, indent=2, cls=PlistEncoder))
db.close()The ZDATA blob is a binary plist that deserializes to a list of action dictionaries (not a dict with WFWorkflowActions key — that's the .shortcut file format).
Each action has:
{
"WFWorkflowActionIdentifier": "is.workflow.actions.gettext",
"WFWorkflowActionParameters": {
"UUID": "...",
"WFTextActionText": "Hello",
"CustomOutputName": "myVar"
}
}import plistlib
shortcut = {
'WFWorkflowMinimumClientVersion': 900,
'WFWorkflowMinimumClientVersionString': '900',
'WFWorkflowClientVersion': '2302.0.4',
'WFWorkflowClientRelease': '2302.0.4',
'WFWorkflowIcon': {
'WFWorkflowIconStartColor': 4282601983, # Color int
'WFWorkflowIconGlyphNumber': 59511, # SF Symbol glyph
},
'WFWorkflowTypes': ['NCWidget', 'WatchKit'],
'WFWorkflowInputContentItemClasses': [
'WFStringContentItem',
'WFGenericFileContentItem',
],
'WFWorkflowActions': [
{
'WFWorkflowActionIdentifier': 'is.workflow.actions.alert',
'WFWorkflowActionParameters': {
'WFAlertActionMessage': 'Hello!',
'WFAlertActionTitle': 'Test',
},
},
],
}
with open('/tmp/my_shortcut.shortcut', 'wb') as f:
plistlib.dump(shortcut, f, fmt=plistlib.FMT_BINARY)shortcuts sign -i /tmp/my_shortcut.shortcut -o /tmp/my_shortcut_signed.shortcut --mode anyoneThe shortcuts sign command outputs harmless ERROR: Unrecognized attribute string flag warnings — ignore them.
# Opens Shortcuts app with import dialog (user must click "Add Shortcut")
open /tmp/my_shortcut_signed.shortcutThere is no way to silently import — Apple requires user confirmation.
Every action has a UUID. Downstream actions reference upstream outputs via OutputUUID. There are two wiring mechanisms:
{
"WFInput": {
"Value": {
"OutputUUID": "AAAA-BBBB-CCCC",
"Type": "ActionOutput",
"OutputName": "Dictated Text"
},
"WFSerializationType": "WFTextTokenAttachment"
}
}Set with setvariable, read with getvariable or inline VariableName references:
{
"VariableName": "api_key",
"Type": "Variable"
}{
"Value": {
"string": "Bearer \ufffc",
"attachmentsByRange": {
"{7, 1}": {
"OutputUUID": "AAAA-BBBB",
"Type": "ActionOutput",
"OutputName": "API_KEY"
}
}
},
"WFSerializationType": "WFTextTokenString"
}The \ufffc (U+FFFC, Object Replacement Character) marks where a variable is inserted. attachmentsByRange maps {position, length} to the variable reference. The position must be exact — count characters in the string field to get the correct offset.
For passing an entire value (not embedded in a string):
{
"Value": {
"OutputUUID": "AAAA-BBBB",
"Type": "ActionOutput",
"OutputName": "Recorded Audio"
},
"WFSerializationType": "WFTextTokenAttachment"
}The is.workflow.actions.downloadurl ("Get Contents of URL") action is used for all HTTP requests. This is critical for integrating with external APIs.
{
'WFWorkflowActionIdentifier': 'is.workflow.actions.downloadurl',
'WFWorkflowActionParameters': {
'UUID': uid(),
'WFURL': 'https://api.openai.com/v1/audio/transcriptions',
'WFHTTPMethod': 'POST',
'WFHTTPBodyType': 'Form',
'ShowHeaders': True,
'WFHTTPHeaders': {
'Value': {'WFDictionaryFieldValueItems': [
# Key-value header items (see dict_item pattern below)
]},
'WFSerializationType': 'WFDictionaryFieldValue',
},
'WFFormValues': {
'Value': {'WFDictionaryFieldValueItems': [
# Form fields — use WFItemType: 5 for file attachments
]},
'WFSerializationType': 'WFDictionaryFieldValue',
},
},
}Use WFHTTPBodyType: 'JSON' with WFJSONValues. This is the proven working pattern — do NOT try to pass JSON as text via WFRequestVariable (it silently fails).
{
'WFHTTPBodyType': 'JSON',
'WFJSONValues': {
'Value': {'WFDictionaryFieldValueItems': [
# model (string)
{'WFKey': tt('model'), 'WFItemType': 0, 'WFValue': tt('gpt-4o-mini')},
# messages (array of dicts)
{
'WFKey': tt('messages'),
'WFItemType': 2, # Array type
'WFValue': {
'Value': [
{
'WFItemType': 1, # Dict type
'WFValue': {
'Value': {
'Value': {'WFDictionaryFieldValueItems': [
{'WFKey': tt('role'), 'WFItemType': 0, 'WFValue': tt('system')},
{'WFKey': tt('content'), 'WFItemType': 0, 'WFValue': tt('You are...')},
]},
'WFSerializationType': 'WFDictionaryFieldValue',
},
'WFSerializationType': 'WFDictionaryFieldValue',
},
},
# ... more message dicts
],
'WFSerializationType': 'WFArrayParameterState',
},
},
]},
'WFSerializationType': 'WFDictionaryFieldValue',
},
}| WFItemType | Meaning |
|---|---|
| 0 | String value |
| 1 | Dictionary (nested) |
| 2 | Array |
| 3 | Number |
| 4 | Boolean |
| 5 | File attachment (for form uploads) |
Chain: downloadurl → getvalueforkey for each nested level.
For OpenAI chat completions (choices[0].message.content):
downloadurl → getvalueforkey "choices" → getvalueforkey "message" → getvalueforkey "content"
Shortcuts auto-extracts the first element when getting a value from an array.
{
'WFKey': tt('file'),
'WFItemType': 5, # File type
'WFValue': {
'Value': {
'Value': {
'OutputUUID': RECORD_UUID,
'Type': 'ActionOutput',
'OutputName': 'Recorded Audio',
},
'WFSerializationType': 'WFTextTokenAttachment',
},
'WFSerializationType': 'WFTokenAttachmentParameterState',
},
}Three actions with matching GroupingIdentifier:
# IF (WFControlFlowMode: 0)
{
'WFWorkflowActionIdentifier': 'is.workflow.actions.conditional',
'WFWorkflowActionParameters': {
'WFInput': {
'Type': 'Variable',
'Variable': oref(SOME_UUID, 'some_output'),
},
'WFControlFlowMode': 0,
'WFCondition': 8, # contains
'WFConditionalActionString': 'YES',
'GroupingIdentifier': IF_GROUP_UUID,
},
},
# ... actions inside the IF block ...
# END IF (WFControlFlowMode: 2)
{
'WFWorkflowActionIdentifier': 'is.workflow.actions.conditional',
'WFWorkflowActionParameters': {
'WFControlFlowMode': 2,
'GroupingIdentifier': IF_GROUP_UUID,
'UUID': uid(),
},
},| Value | Meaning |
|---|---|
| 2 | begins with |
| 3 | ends with |
| 4 | is (equals) |
| 8 | contains |
| 9 | does not contain |
| 99 | is (text comparison) |
| 100 | is not |
| Action | Identifier |
|---|---|
| Text | is.workflow.actions.gettext |
| Show Alert | is.workflow.actions.alert |
| Show Result | is.workflow.actions.showresult |
| Ask for Input | is.workflow.actions.ask |
| Dictate Text | is.workflow.actions.dictatetext |
| Set Variable | is.workflow.actions.setvariable |
| Get Variable | is.workflow.actions.getvariable |
| If/Otherwise/End If | is.workflow.actions.conditional |
| Choose from Menu | is.workflow.actions.choosefrommenu |
| Run Shell Script | is.workflow.actions.runsshscript |
| Open URL | is.workflow.actions.openurl |
| Get URL (HTTP request) | is.workflow.actions.downloadurl |
| Copy to Clipboard | is.workflow.actions.setclipboard |
| Get Dictionary Value | is.workflow.actions.getvalueforkey |
| Detect Text (parse JSON) | is.workflow.actions.detect.text |
| Record Audio | is.workflow.actions.recordaudio |
| Apple Intelligence LLM | is.workflow.actions.askllm (TEXT-ONLY — cannot process audio/images) |
| Append to File | is.workflow.actions.file.append |
| Get My Shortcuts | is.workflow.actions.getmyworkflows |
| Exit Shortcut | is.workflow.actions.exit |
| Ask Claude (iOS) | com.anthropic.claude.ClaudeAppIntentsExtension (parameter: message lowercase) |
| Open Claude (iOS) | com.anthropic.claude.OpenClaudeIntent (just opens app, no text input) |
| Create Linear Issue (iOS) | com.linear.ios.CreateIssueIntent |
| App Intent (3rd party) | com.{bundle}.{IntentName} |
-
Ask Claude:
com.anthropic.claude.ClaudeAppIntentsExtension- Parameter key:
message(lowercase — case-sensitive!) Message(capitalized) does NOT work- Shows as "Unknown Action" on macOS — only resolves on iOS
- May timeout on long tasks — Claude continues processing in the app regardless
- Parameter key:
-
Open Claude:
com.anthropic.claude.OpenClaudeIntent- Just opens the app, does not accept text input
To find the TeamIdentifier and BundleIdentifier for any app, decompile an existing shortcut that uses that app's intent from the Shortcuts database.
'AppIntentDescriptor': {
'TeamIdentifier': 'TEAM_ID',
'BundleIdentifier': 'com.example.app',
'Name': 'App Name',
'AppIntentIdentifier': 'IntentName',
},You cannot inspect iOS app intent parameters from macOS. To find the correct parameter name:
- Import a shortcut with the intent on iOS
- Open it in the Shortcuts editor — the parameter label is visible (e.g., "Message")
- Try lowercase version of the label first (e.g.,
message) - If that doesn't work, try the exact label casing
Always validate API keys at the start of shortcuts that use external APIs:
# Text action with placeholder key
{'WFWorkflowActionIdentifier': 'is.workflow.actions.gettext',
'WFWorkflowActionParameters': {'UUID': KEY_UUID, 'CustomOutputName': 'API_KEY',
'WFTextActionText': 'PASTE_YOUR_API_KEY_HERE'}},
# IF key equals placeholder → alert and exit
{'WFWorkflowActionIdentifier': 'is.workflow.actions.conditional',
'WFWorkflowActionParameters': {
'WFInput': {'Type': 'Variable', 'Variable': oref(KEY_UUID, 'API_KEY')},
'WFControlFlowMode': 0, 'WFCondition': 4,
'WFConditionalActionString': 'PASTE_YOUR_API_KEY_HERE',
'GroupingIdentifier': IF_KEY_GROUP}},
{'WFWorkflowActionIdentifier': 'is.workflow.actions.alert',
'WFWorkflowActionParameters': {'WFAlertActionTitle': 'No API Key',
'WFAlertActionMessage': 'Open this shortcut and paste your API key in the first Text action.'}},
{'WFWorkflowActionIdentifier': 'is.workflow.actions.exit',
'WFWorkflowActionParameters': {}},
{'WFWorkflowActionIdentifier': 'is.workflow.actions.conditional',
'WFWorkflowActionParameters': {'WFControlFlowMode': 2, 'GroupingIdentifier': IF_KEY_GROUP, 'UUID': uid()}},UPDATE ZSHORTCUT SET ZNAME = 'New Name' WHERE Z_PK = ?;UPDATE ZSHORTCUTICON SET ZBACKGROUNDCOLORVALUE = ?, ZGLYPHNUMBER = ? WHERE Z_PK = ?;# Must delete from all related tables
cur.execute("DELETE FROM ZSHORTCUTACTIONS WHERE Z_PK = ?", (actions_pk,))
cur.execute("DELETE FROM ZSHORTCUTRUNEVENT WHERE ZSHORTCUT = ?", (pk,))
cur.execute("DELETE FROM ZTRIGGER WHERE ZSHORTCUT = ?", (pk,))
cur.execute("DELETE FROM ZSHORTCUTICON WHERE Z_PK = ?", (icon_pk,))
cur.execute("DELETE FROM ZSHORTCUT WHERE Z_PK = ?", (pk,))# Get next PKs
cur.execute("SELECT COALESCE(MAX(Z_PK), 0) + 1 FROM ZSHORTCUTACTIONS")
actions_pk = cur.fetchone()[0]
# ... same for ZSHORTCUT and ZSHORTCUTICON
# Insert icon, actions, shortcut (in order — FKs must exist)
cur.execute("INSERT INTO ZSHORTCUTICON (Z_PK, Z_ENT, Z_OPT, ZGLYPHNUMBER, ZBACKGROUNDCOLORVALUE) VALUES (?, 3, 1, ?, ?)", ...)
cur.execute("INSERT INTO ZSHORTCUTACTIONS (Z_PK, Z_ENT, Z_OPT, ZDATA, ZSHORTCUT) VALUES (?, 2, 1, ?, ?)", ...)
cur.execute("INSERT INTO ZSHORTCUT (Z_PK, Z_ENT, Z_OPT, ZACTIONCOUNT, ZACTIONS, ZICON, ...) VALUES (...)", ...)
# Link icon back
cur.execute("UPDATE ZSHORTCUTICON SET ZWORKFLOW = ? WHERE Z_PK = ?", (shortcut_pk, icon_pk))- ZDATA is a list, not a dict — unlike
.shortcutfiles which wrap actions inWFWorkflowActions - iCloud sync restores deleted shortcuts — deleting from SQLite doesn't propagate to CloudKit. Delete from the Shortcuts app UI for permanent removal.
- DB-inserted shortcuts may not appear in CLI —
siriactionsdcaches the DB. Import via signed file (open file.shortcut) is more reliable. - Always back up before modifying:
cp ~/Library/Shortcuts/Shortcuts.sqlite /tmp/Shortcuts_backup_$(date +%Y%m%d_%H%M%S).sqlite
- iOS app intents show as "Unknown Action" on macOS — expected behavior. They only resolve on the target device.
- iOS app intent parameter names are case-sensitive — Claude uses
message(lowercase), notMessage. - Cannot verify iOS shortcuts on macOS — actions render as "Unknown Action". Must test on iPhone.
is.workflow.actions.transcribeaudiomay require newer iOS versions — shows "Unknown Action" on older versions.
- Use
WFJSONValuesfor JSON request bodies — do NOT tryWFRequestVariableor text-based JSON bodies. They silently fail. TheWFJSONValuesnested dict/array pattern is the only proven working approach. WFHTTPBodyType: 'Form'for multipart (file uploads).WFHTTPBodyType: 'JSON'for JSON bodies.- API keys in shortcuts are stored in plaintext in the ZDATA blob — visible to anyone with FDA.
- Apple Intelligence
askllmis text-only — cannot process audio, images, or files. Usedictatetextfor real-time speech-to-text. - Claude iOS intent may timeout on long tasks — the
ClaudeAppIntentsExtensionfires the message but returns before Claude finishes. Claude continues working in the app regardless. is.workflow.actions.runsshscriptis the identifier for "Run Shell Script" (not SSH — legacy naming).shortcuts runrequires the app's actions to be available — some shortcuts fail headless if they need UI.- Signed shortcuts can't be decompiled from the
.shortcutfile alone — use SQLite extraction instead.
import uuid
def uid():
return str(uuid.uuid4()).upper()
def tt(s, att=None):
"""Text token (WFTextTokenString)"""
v = {'string': s}
if att: v['attachmentsByRange'] = att
return {'Value': v, 'WFSerializationType': 'WFTextTokenString'}
def oref(u, n):
"""Output reference (WFTextTokenAttachment)"""
return {'Value': {'OutputUUID': u, 'Type': 'ActionOutput', 'OutputName': n},
'WFSerializationType': 'WFTextTokenAttachment'}
def dv(items):
"""Dictionary value wrapper"""
return {'Value': {'WFDictionaryFieldValueItems': items},
'WFSerializationType': 'WFDictionaryFieldValue'}
def di(k, v_str):
"""Dictionary item (string key → string value)"""
return {'WFKey': tt(k), 'WFItemType': 0, 'WFValue': tt(v_str)}Settings → Action Button → swipe to Shortcut → select your shortcut.
Settings → Accessibility → Touch → Back Tap:
- Double Tap → select a shortcut
- Triple Tap → select a shortcut
Limited scripting dictionary — can list, run, and get metadata, but cannot read actions or modify shortcuts:
tell application "Shortcuts Events" to get name of every shortcut
tell application "Shortcuts Events" to run shortcut "Name" with input "text"shortcuts list # List all
shortcuts run "Name" # Run (headless)
shortcuts run "Name" -i input.txt # Run with file input
shortcuts run "Name" -i input.txt -o out.txt # Run with input and output
shortcuts view "Name" # Open in Shortcuts editor
shortcuts sign -i file -o file --mode anyone # Sign for sharingNo export or create subcommands exist.
- 0xdevalias gist — Decompilation reference (SQLite access, plist format)
- Shortcuts File Format — Plist structure docs