Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 

Repository files navigation

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
filePattern bashPattern
*.shortcut
shortcuts|siri.*shortcut

Apple Shortcuts — Programmatic Control

Prerequisites

  • Full Disk Access must be enabled for the terminal app (System Settings → Privacy & Security → Full Disk Access)
  • Without FDA, ~/Library/Shortcuts/Shortcuts.sqlite is TCC-protected and inaccessible
  • Python 3 with plistlib (built-in) and sqlite3 (built-in) — no extra packages needed
  • shortcuts CLI (built into macOS) — for signing and listing

Database Location

~/Library/Shortcuts/Shortcuts.sqlite

Key Tables

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

Schema (important columns)

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)

CoreData Triggers

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)

CoreData Timestamps

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')

Reading / Decompiling Shortcuts

List all shortcuts

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;"

Extract and decompile a shortcut's actions

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

ZDATA format

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"
  }
}

Creating Shortcuts

Python + plistlib (recommended)

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)

Signing (required for import)

shortcuts sign -i /tmp/my_shortcut.shortcut -o /tmp/my_shortcut_signed.shortcut --mode anyone

The shortcuts sign command outputs harmless ERROR: Unrecognized attribute string flag warnings — ignore them.

Importing

# Opens Shortcuts app with import dialog (user must click "Add Shortcut")
open /tmp/my_shortcut_signed.shortcut

There is no way to silently import — Apple requires user confirmation.

Data Flow — How Actions Connect

Every action has a UUID. Downstream actions reference upstream outputs via OutputUUID. There are two wiring mechanisms:

1. UUID Output References (primary — like piping)

{
  "WFInput": {
    "Value": {
      "OutputUUID": "AAAA-BBBB-CCCC",
      "Type": "ActionOutput",
      "OutputName": "Dictated Text"
    },
    "WFSerializationType": "WFTextTokenAttachment"
  }
}

2. Named Variables (global — like env vars)

Set with setvariable, read with getvariable or inline VariableName references:

{
  "VariableName": "api_key",
  "Type": "Variable"
}

Text with Embedded Variables (WFTextTokenString)

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

Direct Attachment (WFTextTokenAttachment)

For passing an entire value (not embedded in a string):

{
  "Value": {
    "OutputUUID": "AAAA-BBBB",
    "Type": "ActionOutput",
    "OutputName": "Recorded Audio"
  },
  "WFSerializationType": "WFTextTokenAttachment"
}

Making HTTP API Calls (downloadurl)

The is.workflow.actions.downloadurl ("Get Contents of URL") action is used for all HTTP requests. This is critical for integrating with external APIs.

Form Data (multipart — for file uploads like Whisper API)

{
    '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',
        },
    },
}

JSON Body (for chat completions, REST APIs)

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 values for dict items

WFItemType Meaning
0 String value
1 Dictionary (nested)
2 Array
3 Number
4 Boolean
5 File attachment (for form uploads)

Parsing JSON API Responses

Chain: downloadurlgetvalueforkey 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.

File Attachment in Form Data

{
    'WFKey': tt('file'),
    'WFItemType': 5,  # File type
    'WFValue': {
        'Value': {
            'Value': {
                'OutputUUID': RECORD_UUID,
                'Type': 'ActionOutput',
                'OutputName': 'Recorded Audio',
            },
            'WFSerializationType': 'WFTextTokenAttachment',
        },
        'WFSerializationType': 'WFTokenAttachmentParameterState',
    },
}

Conditional Logic (If/Otherwise/End If)

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(),
    },
},

WFCondition values

Value Meaning
2 begins with
3 ends with
4 is (equals)
8 contains
9 does not contain
99 is (text comparison)
100 is not

Common Action Identifiers

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}

iOS App Intents

Claude iOS App

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

App Intent Descriptor (required for all 3rd-party intents)

'AppIntentDescriptor': {
    'TeamIdentifier': 'TEAM_ID',
    'BundleIdentifier': 'com.example.app',
    'Name': 'App Name',
    'AppIntentIdentifier': 'IntentName',
},

Discovering Intent Parameters

You cannot inspect iOS app intent parameters from macOS. To find the correct parameter name:

  1. Import a shortcut with the intent on iOS
  2. Open it in the Shortcuts editor — the parameter label is visible (e.g., "Message")
  3. Try lowercase version of the label first (e.g., message)
  4. If that doesn't work, try the exact label casing

API Key Validation Pattern

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()}},

Modifying Shortcuts in the Database

Renaming

UPDATE ZSHORTCUT SET ZNAME = 'New Name' WHERE Z_PK = ?;

Changing Icon

UPDATE ZSHORTCUTICON SET ZBACKGROUNDCOLORVALUE = ?, ZGLYPHNUMBER = ? WHERE Z_PK = ?;

Deleting (with cleanup)

# 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,))

Inserting New Shortcuts

# 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))

Gotchas

Database & Sync

  • ZDATA is a list, not a dict — unlike .shortcut files which wrap actions in WFWorkflowActions
  • 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 CLIsiriactionsd caches 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 vs macOS

  • 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), not Message.
  • Cannot verify iOS shortcuts on macOS — actions render as "Unknown Action". Must test on iPhone.
  • is.workflow.actions.transcribeaudio may require newer iOS versions — shows "Unknown Action" on older versions.

API Calls

  • Use WFJSONValues for JSON request bodies — do NOT try WFRequestVariable or text-based JSON bodies. They silently fail. The WFJSONValues nested 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.

Actions

  • Apple Intelligence askllm is text-only — cannot process audio, images, or files. Use dictatetext for real-time speech-to-text.
  • Claude iOS intent may timeout on long tasks — the ClaudeAppIntentsExtension fires the message but returns before Claude finishes. Claude continues working in the app regardless.
  • is.workflow.actions.runsshscript is the identifier for "Run Shell Script" (not SSH — legacy naming).
  • shortcuts run requires the app's actions to be available — some shortcuts fail headless if they need UI.
  • Signed shortcuts can't be decompiled from the .shortcut file alone — use SQLite extraction instead.

Helper Functions for Building Shortcuts

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)}

iPhone Hardware Integration

Action Button (iPhone 15 Pro+)

Settings → Action Button → swipe to Shortcut → select your shortcut.

Back Tap (any iPhone with iOS 14+)

Settings → Accessibility → Touch → Back Tap:

  • Double Tap → select a shortcut
  • Triple Tap → select a shortcut

AppleScript / JXA Access

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"

CLI

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 sharing

No export or create subcommands exist.

References

About

Programmatic control of Apple Shortcuts — read, create, edit, and decompile macOS/iOS Shortcuts via Python + SQLite

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors