shortcuts-playground 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +54 -0
  2. package/agents/shortcut-builder.md +172 -0
  3. package/agents/shortcut-remixer.md +239 -0
  4. package/commands/build-shortcut.md +32 -0
  5. package/commands/remix-shortcut.md +34 -0
  6. package/opencode.json +30 -0
  7. package/package.json +47 -0
  8. package/plugin/validator.ts +179 -0
  9. package/skills/shortcuts-playground/ACTIONS.md +713 -0
  10. package/skills/shortcuts-playground/APPINTENTS.md +2760 -0
  11. package/skills/shortcuts-playground/AUTOMATION_TRIGGERS.md +133 -0
  12. package/skills/shortcuts-playground/BEST_PRACTICES.md +357 -0
  13. package/skills/shortcuts-playground/CHANGELOG.md +585 -0
  14. package/skills/shortcuts-playground/CONTROL_FLOW.md +777 -0
  15. package/skills/shortcuts-playground/DATE_TIME.md +75 -0
  16. package/skills/shortcuts-playground/EXAMPLES.md +738 -0
  17. package/skills/shortcuts-playground/FILTERS.md +697 -0
  18. package/skills/shortcuts-playground/HEALTHKIT.md +317 -0
  19. package/skills/shortcuts-playground/ICONS_AND_COLORS.md +89 -0
  20. package/skills/shortcuts-playground/JAVASCRIPT_WEBPAGE.md +51 -0
  21. package/skills/shortcuts-playground/PARAMETER_TYPES.md +1117 -0
  22. package/skills/shortcuts-playground/PLIST_FORMAT.md +296 -0
  23. package/skills/shortcuts-playground/README.md +77 -0
  24. package/skills/shortcuts-playground/SKILL.md +459 -0
  25. package/skills/shortcuts-playground/THIRD_PARTY_ACTIONS.md +77 -0
  26. package/skills/shortcuts-playground/TOOLKIT_SNAPSHOT.md +62 -0
  27. package/skills/shortcuts-playground/URL_SCHEMES.md +59 -0
  28. package/skills/shortcuts-playground/VARIABLES.md +569 -0
  29. package/skills/shortcuts-playground/assets/shortcuts-small.svg +14 -0
  30. package/skills/shortcuts-playground/assets/shortcuts.png +0 -0
  31. package/skills/shortcuts-playground/data/healthkit-ios26.2-reference.json +2603 -0
  32. package/skills/shortcuts-playground/data/macos27-shortpy-grounding.json +4085 -0
  33. package/skills/shortcuts-playground/data/macos27-workflow-trigger-samples.json +1 -0
  34. package/skills/shortcuts-playground/data/shortcuts-glyph-synonyms.json +5102 -0
  35. package/skills/shortcuts-playground/data/shortcuts-icon-colors.json +107 -0
  36. package/skills/shortcuts-playground/data/shortcuts-official-glyph-mapping.json +509 -0
  37. package/skills/shortcuts-playground/data/toolkit-v63-tool-ids.json +1806 -0
  38. package/skills/shortcuts-playground/data/toolkit-v78-first-party-enum-cases.json +1 -0
  39. package/skills/shortcuts-playground/data/toolkit-v78-first-party-parameter-keys.json +1 -0
  40. package/skills/shortcuts-playground/data/toolkit-v78-ios27-tool-ids.json +1222 -0
  41. package/skills/shortcuts-playground/data/toolkit-v78-tool-ids.json +2745 -0
  42. package/skills/shortcuts-playground/data/toolkit-v78-trigger-parameter-keys.json +1051 -0
  43. package/skills/shortcuts-playground/golden-shortcuts/index.jsonl +19 -0
  44. package/skills/shortcuts-playground/golden-shortcuts/xml/1be4dde95b794253bf82438e201b33e7.xml +174 -0
  45. package/skills/shortcuts-playground/golden-shortcuts/xml/2e0fb675e45948aaacee7e534f910492.xml +1016 -0
  46. package/skills/shortcuts-playground/golden-shortcuts/xml/332c12a0060043b388b22b806be7ab58.xml +1924 -0
  47. package/skills/shortcuts-playground/golden-shortcuts/xml/3dd4ee24e43f464f92adaa70a0311eaa.xml +1177 -0
  48. package/skills/shortcuts-playground/golden-shortcuts/xml/4d102301c6e646faa7a8a221f4f4ec98.xml +321 -0
  49. package/skills/shortcuts-playground/golden-shortcuts/xml/51cc4e26d1044893a0c3f2f3630cf2d2.xml +1933 -0
  50. package/skills/shortcuts-playground/golden-shortcuts/xml/623e7f1ca5f948e2bd53811fec63e544.xml +136 -0
  51. package/skills/shortcuts-playground/golden-shortcuts/xml/6a18b76843ac45c384ac3400f3740997.xml +1791 -0
  52. package/skills/shortcuts-playground/golden-shortcuts/xml/71f0cacb0f604b399b76c5dcb7286e7c.xml +2228 -0
  53. package/skills/shortcuts-playground/golden-shortcuts/xml/807525ed9f974829bc8494defac923a8.xml +259 -0
  54. package/skills/shortcuts-playground/golden-shortcuts/xml/8ab0d39826ab4249be58672763caa3ba.xml +420 -0
  55. package/skills/shortcuts-playground/golden-shortcuts/xml/91c45fd3fd3b427897d9ba485efb1227.xml +800 -0
  56. package/skills/shortcuts-playground/golden-shortcuts/xml/97be626bb25c41709d175646a7f6d8f2.xml +424 -0
  57. package/skills/shortcuts-playground/golden-shortcuts/xml/ae59e10d409348f9bd33894f03f9beb4.xml +568 -0
  58. package/skills/shortcuts-playground/golden-shortcuts/xml/afa83b6be811483b9c32189c41eb9312.xml +391 -0
  59. package/skills/shortcuts-playground/golden-shortcuts/xml/cfdb083b857e4ac189629fa386d27cdc.xml +193 -0
  60. package/skills/shortcuts-playground/golden-shortcuts/xml/e01cebe192d64b2fbca80204d03d92ab.xml +111 -0
  61. package/skills/shortcuts-playground/golden-shortcuts/xml/ef669bcf8bad489d9ef4b88bfaf5772f.xml +780 -0
  62. package/skills/shortcuts-playground/golden-shortcuts/xml/f44f5caf5e3e48d4817e73af450c4404.xml +570 -0
  63. package/skills/shortcuts-playground/scripts/generate_healthkit_reference.py +394 -0
  64. package/skills/shortcuts-playground/scripts/lookup_action_grounding.py +1164 -0
  65. package/skills/shortcuts-playground/scripts/select_shortcut_icon_color.py +597 -0
  66. package/skills/shortcuts-playground/scripts/test_random_mixed_shortcuts.py +1067 -0
  67. package/skills/shortcuts-playground/scripts/test_wiring_regressions.py +2247 -0
  68. package/skills/shortcuts-playground/scripts/validate_shortcut.py +4526 -0
@@ -0,0 +1,777 @@
1
+ # Control Flow Patterns
2
+
3
+ How to implement loops, conditionals, and menus in Shortcuts.
4
+
5
+ ## Overview
6
+
7
+ Control flow actions use two key parameters:
8
+ - **GroupingIdentifier**: A UUID that links related actions (start, middle, end)
9
+ - **WFControlFlowMode**: An integer indicating the action's role
10
+ - `0` = Start (begin block)
11
+ - `1` = Middle (else, case)
12
+ - `2` = End (close block)
13
+
14
+ **Important**: `WFControlFlowMode` must be an `<integer>`, not a `<string>`.
15
+
16
+ ---
17
+
18
+ ## Repeat Count
19
+
20
+ Repeat a block of actions a specific number of times.
21
+
22
+ ### Structure
23
+ | Mode | Action | Description |
24
+ |------|--------|-------------|
25
+ | 0 | Start | Begin repeat, set count |
26
+ | 2 | End | Close repeat block |
27
+
28
+ ### Template
29
+
30
+ ```xml
31
+ <!-- Repeat Start -->
32
+ <dict>
33
+ <key>WFWorkflowActionIdentifier</key>
34
+ <string>is.workflow.actions.repeat.count</string>
35
+ <key>WFWorkflowActionParameters</key>
36
+ <dict>
37
+ <key>GroupingIdentifier</key>
38
+ <string>REPEAT-GROUP-UUID</string>
39
+ <key>WFControlFlowMode</key>
40
+ <integer>0</integer>
41
+ <key>WFRepeatCount</key>
42
+ <!-- Can be integer or variable reference -->
43
+ <integer>5</integer>
44
+ </dict>
45
+ </dict>
46
+
47
+ <!-- Actions inside the loop go here -->
48
+
49
+ <!-- Repeat End -->
50
+ <dict>
51
+ <key>WFWorkflowActionIdentifier</key>
52
+ <string>is.workflow.actions.repeat.count</string>
53
+ <key>WFWorkflowActionParameters</key>
54
+ <dict>
55
+ <key>UUID</key>
56
+ <string>END-ACTION-UUID</string>
57
+ <key>GroupingIdentifier</key>
58
+ <string>REPEAT-GROUP-UUID</string>
59
+ <key>WFControlFlowMode</key>
60
+ <integer>2</integer>
61
+ </dict>
62
+ </dict>
63
+ ```
64
+
65
+ ### Accessing Repeat Index
66
+
67
+ Inside the loop, reference the current index as a **named Variable** (Type=Variable, not ActionOutput):
68
+
69
+ ```xml
70
+ <key>attachmentsByRange</key>
71
+ <dict>
72
+ <key>{0, 1}</key>
73
+ <dict>
74
+ <key>Type</key>
75
+ <string>Variable</string>
76
+ <key>VariableName</key>
77
+ <string>Repeat Index</string>
78
+ </dict>
79
+ </dict>
80
+ ```
81
+
82
+ **CRITICAL:** Repeat Index uses `Type: Variable` with `VariableName: "Repeat Index"`, NOT `Type: ActionOutput` referencing the end action's UUID. Using the wrong type causes the variable to appear as "Repeat Results" in the UI and fails at runtime.
83
+
84
+ ---
85
+
86
+ ## Repeat with Each (For Each)
87
+
88
+ Iterate over each item in a list.
89
+
90
+ ### Structure
91
+ | Mode | Action | Description |
92
+ |------|--------|-------------|
93
+ | 0 | Start | Begin loop, specify input list |
94
+ | 2 | End | Close loop |
95
+
96
+ ### Template
97
+
98
+ ```xml
99
+ <!-- Repeat Each Start -->
100
+ <dict>
101
+ <key>WFWorkflowActionIdentifier</key>
102
+ <string>is.workflow.actions.repeat.each</string>
103
+ <key>WFWorkflowActionParameters</key>
104
+ <dict>
105
+ <key>GroupingIdentifier</key>
106
+ <string>FOREACH-GROUP-UUID</string>
107
+ <key>WFControlFlowMode</key>
108
+ <integer>0</integer>
109
+ <key>WFInput</key>
110
+ <dict>
111
+ <key>Value</key>
112
+ <dict>
113
+ <key>OutputUUID</key>
114
+ <string>LIST-SOURCE-UUID</string>
115
+ <key>OutputName</key>
116
+ <string>List</string>
117
+ <key>Type</key>
118
+ <string>ActionOutput</string>
119
+ </dict>
120
+ <key>WFSerializationType</key>
121
+ <string>WFTextTokenAttachment</string>
122
+ </dict>
123
+ </dict>
124
+ </dict>
125
+
126
+ <!-- Actions inside the loop go here -->
127
+
128
+ <!-- Repeat Each End -->
129
+ <dict>
130
+ <key>WFWorkflowActionIdentifier</key>
131
+ <string>is.workflow.actions.repeat.each</string>
132
+ <key>WFWorkflowActionParameters</key>
133
+ <dict>
134
+ <key>UUID</key>
135
+ <string>END-ACTION-UUID</string>
136
+ <key>GroupingIdentifier</key>
137
+ <string>FOREACH-GROUP-UUID</string>
138
+ <key>WFControlFlowMode</key>
139
+ <integer>2</integer>
140
+ </dict>
141
+ </dict>
142
+ ```
143
+
144
+ ### Accessing Current Item
145
+
146
+ Reference the current item using a **named Variable** (Type=Variable):
147
+
148
+ ```xml
149
+ <key>attachmentsByRange</key>
150
+ <dict>
151
+ <key>{0, 1}</key>
152
+ <dict>
153
+ <key>Type</key>
154
+ <string>Variable</string>
155
+ <key>VariableName</key>
156
+ <string>Repeat Item</string>
157
+ </dict>
158
+ </dict>
159
+ ```
160
+
161
+ **Alternatively** (less common), use the Start action's UUID as an ActionOutput:
162
+
163
+ ```xml
164
+ <key>attachmentsByRange</key>
165
+ <dict>
166
+ <key>{0, 1}</key>
167
+ <dict>
168
+ <key>OutputUUID</key>
169
+ <string>START-ACTION-UUID</string>
170
+ <key>OutputName</key>
171
+ <string>Repeat Item</string>
172
+ <key>Type</key>
173
+ <string>ActionOutput</string>
174
+ </dict>
175
+ </dict>
176
+ ```
177
+
178
+ **Preferred approach:** Use `Type: Variable` with `VariableName: "Repeat Item"` for consistency and UI stability.
179
+
180
+ ---
181
+
182
+ ## Conditional (If/Otherwise/Otherwise If)
183
+
184
+ Execute different actions based on a condition.
185
+
186
+ ### Structure
187
+ | Mode | Action | Description |
188
+ |------|--------|-------------|
189
+ | 0 | If | Start conditional, define condition |
190
+ | 1 | Otherwise / Otherwise If | Else branch, or a conditional middle branch when condition fields are present |
191
+ | 2 | End If | Close conditional |
192
+
193
+ ### Template (single-condition If)
194
+
195
+ ```xml
196
+ <!-- If -->
197
+ <dict>
198
+ <key>WFWorkflowActionIdentifier</key>
199
+ <string>is.workflow.actions.conditional</string>
200
+ <key>WFWorkflowActionParameters</key>
201
+ <dict>
202
+ <key>GroupingIdentifier</key>
203
+ <string>IF-GROUP-UUID</string>
204
+ <key>WFControlFlowMode</key>
205
+ <integer>0</integer>
206
+ <key>WFCondition</key>
207
+ <integer>4</integer>
208
+ <key>WFConditionalActionString</key>
209
+ <string>Yes</string>
210
+ <key>WFInput</key>
211
+ <dict>
212
+ <key>Type</key>
213
+ <string>Variable</string>
214
+ <key>Variable</key>
215
+ <dict>
216
+ <key>Value</key>
217
+ <dict>
218
+ <key>OutputName</key>
219
+ <string>Text</string>
220
+ <key>OutputUUID</key>
221
+ <string>VALUE-TO-TEST-UUID</string>
222
+ <key>Type</key>
223
+ <string>ActionOutput</string>
224
+ </dict>
225
+ <key>WFSerializationType</key>
226
+ <string>WFTextTokenAttachment</string>
227
+ </dict>
228
+ </dict>
229
+ </dict>
230
+ </dict>
231
+
232
+ <!-- Actions for the "If" branch go here -->
233
+
234
+ <!-- Otherwise -->
235
+ <dict>
236
+ <key>WFWorkflowActionIdentifier</key>
237
+ <string>is.workflow.actions.conditional</string>
238
+ <key>WFWorkflowActionParameters</key>
239
+ <dict>
240
+ <key>GroupingIdentifier</key>
241
+ <string>IF-GROUP-UUID</string>
242
+ <key>WFControlFlowMode</key>
243
+ <integer>1</integer>
244
+ </dict>
245
+ </dict>
246
+
247
+ <!-- Actions for the "Otherwise" branch go here -->
248
+
249
+ <!-- End If -->
250
+ <dict>
251
+ <key>WFWorkflowActionIdentifier</key>
252
+ <string>is.workflow.actions.conditional</string>
253
+ <key>WFWorkflowActionParameters</key>
254
+ <dict>
255
+ <key>GroupingIdentifier</key>
256
+ <string>IF-GROUP-UUID</string>
257
+ <key>WFControlFlowMode</key>
258
+ <integer>2</integer>
259
+ </dict>
260
+ </dict>
261
+ ```
262
+
263
+ ### Otherwise If (macOS 27+)
264
+
265
+ `Otherwise If` is not a new action identifier and does not use a new control-flow mode. It is still `is.workflow.actions.conditional` with `WFControlFlowMode = 1`; the difference from plain Otherwise is that it also carries the same condition fields as an If start (`WFCondition`, `WFInput`, and the required literal field for that condition code).
266
+
267
+ Plain Otherwise:
268
+
269
+ ```xml
270
+ <key>WFControlFlowMode</key>
271
+ <integer>1</integer>
272
+ ```
273
+
274
+ Otherwise If:
275
+
276
+ ```xml
277
+ <dict>
278
+ <key>WFWorkflowActionIdentifier</key>
279
+ <string>is.workflow.actions.conditional</string>
280
+ <key>WFWorkflowActionParameters</key>
281
+ <dict>
282
+ <key>GroupingIdentifier</key>
283
+ <string>IF-GROUP-UUID</string>
284
+ <key>WFControlFlowMode</key>
285
+ <integer>1</integer>
286
+ <key>WFCondition</key>
287
+ <integer>99</integer>
288
+ <key>WFConditionalActionString</key>
289
+ <string>Four</string>
290
+ <key>WFInput</key>
291
+ <dict>
292
+ <key>Type</key>
293
+ <string>Variable</string>
294
+ <key>Variable</key>
295
+ <dict>
296
+ <key>Value</key>
297
+ <dict>
298
+ <key>Type</key>
299
+ <string>Variable</string>
300
+ <key>VariableName</key>
301
+ <string>List Variable</string>
302
+ </dict>
303
+ <key>WFSerializationType</key>
304
+ <string>WFTextTokenAttachment</string>
305
+ </dict>
306
+ </dict>
307
+ </dict>
308
+ </dict>
309
+ ```
310
+
311
+ Place any `Otherwise If` actions after the initial mode 0 If branch and before a final plain Otherwise. Use the same `GroupingIdentifier` for every branch in the block.
312
+
313
+ ### macOS 27 List Contains Import Trap
314
+
315
+ When checking whether a list contains an item, avoid this pattern:
316
+
317
+ 1. Set `Fruit List` from a List action.
318
+ 2. Add an item to `Fruit List`.
319
+ 3. Set `Fruit List` again from the Add to List output.
320
+ 4. Run `If Fruit List contains "Orange"`.
321
+
322
+ On macOS 27, imported shortcuts using that repeated-name list pattern can show a blank comparison chip even though the plist contains `WFConditionalActionString`. Use one of these safe shapes instead:
323
+
324
+ - Reference the final `List`/`Add to List` action output directly in the conditional `WFInput`.
325
+ - Or use an intermediate name while mutating (for example `Working Fruit List`) and assign the final list once to a fresh name (`Fruit List`) immediately before the If.
326
+
327
+ ### Condition Codes (DEFINITIVE — verified against an Apple-built sample shortcut)
328
+
329
+ **ALWAYS use integer codes** for `WFCondition`. String names may import but degrade at runtime.
330
+
331
+ | Code | UI label | Category | Required extra fields |
332
+ |------|----------|----------|------------------------|
333
+ | `0` | is less than | numeric | `WFNumberValue` |
334
+ | `1` | is less than or equal to | numeric | `WFNumberValue` |
335
+ | `2` | is greater than | numeric | `WFNumberValue` |
336
+ | `3` | is greater than or equal to | numeric | `WFNumberValue` |
337
+ | `4` | is (string equals) | string | `WFConditionalActionString` |
338
+ | `5` | is not | string | `WFConditionalActionString` |
339
+ | `8` | begins with | string | `WFConditionalActionString` |
340
+ | `9` | ends with | string | `WFConditionalActionString` |
341
+ | `99` | contains | string | `WFConditionalActionString` |
342
+ | `100`| has any value | existence | (none) |
343
+ | `101`| does not have any value | existence | (none) |
344
+ | `999`| does not contain | string | `WFConditionalActionString` |
345
+ | `1003` | is between | numeric | `WFNumberValue` (lower) + `WFAnotherNumber` (upper) |
346
+
347
+ ⚠️ **Common confusions to avoid:**
348
+ - Code `4` is the string `is` (exact equality). Code `99` is the string `contains` (substring). They are NOT interchangeable.
349
+ - Code `0` is `is less than`, NOT `equals`. There is no numeric "equals" code in the modern Shortcuts conditional action — to check equality of two numbers, use `is greater than or equal to` AND `is less than or equal to` (Any-of-two pattern below) or compare via Text equality (code `4`).
350
+ - Code `2` is `is greater than`, code `3` is `is greater than or equal to`. They differ by inclusivity — easy to swap by mistake.
351
+
352
+ ### Input rule (uniform across all codes)
353
+
354
+ **ALL conditional codes require an explicit `WFInput` as a `Type=Variable` wrapper.** There is no "implicit input" mode. The previous documentation that told you numeric conditions (0–3) use implicit input was incorrect — verified against an Apple-built sample where every single `is.workflow.actions.conditional` action sets `WFInput` explicitly.
355
+
356
+ The wrapper structure:
357
+
358
+ ```xml
359
+ <key>WFInput</key>
360
+ <dict>
361
+ <key>Type</key>
362
+ <string>Variable</string>
363
+ <key>Variable</key>
364
+ <dict>
365
+ <key>Value</key>
366
+ <dict>
367
+ <key>OutputName</key>
368
+ <string>Text</string>
369
+ <key>OutputUUID</key>
370
+ <string>UPSTREAM-ACTION-UUID</string>
371
+ <key>Type</key>
372
+ <string>ActionOutput</string>
373
+ </dict>
374
+ <key>WFSerializationType</key>
375
+ <string>WFTextTokenAttachment</string>
376
+ </dict>
377
+ </dict>
378
+ ```
379
+
380
+ For `Type=Variable` referencing a named variable instead of an `ActionOutput`, replace the inner `Value` dict with:
381
+
382
+ ```xml
383
+ <key>Value</key>
384
+ <dict>
385
+ <key>Type</key>
386
+ <string>Variable</string>
387
+ <key>VariableName</key>
388
+ <string>MyVarName</string>
389
+ </dict>
390
+ ```
391
+
392
+ **Placement rules:**
393
+ - `WFInput` belongs on Mode 0 (If start) and on Mode 1 only when the action is an Otherwise If. Plain Otherwise (mode 1 with no condition fields) and End If (mode 2) carry only `GroupingIdentifier` + `WFControlFlowMode`.
394
+ - The `WFInput` field itself is a dict with `Type` = `"Variable"` and a nested `Variable` dict. Bare `WFTextTokenAttachment` (without the `Type`/`Variable` wrapper) imports as blank in the editor.
395
+
396
+ ### "is between" (code 1003)
397
+
398
+ The between condition needs both a lower bound (`WFNumberValue`, a literal string) and an upper bound (`WFAnotherNumber`, a token attachment that can hold either a literal or a variable reference):
399
+
400
+ ```xml
401
+ <dict>
402
+ <key>WFWorkflowActionIdentifier</key>
403
+ <string>is.workflow.actions.conditional</string>
404
+ <key>WFWorkflowActionParameters</key>
405
+ <dict>
406
+ <key>GroupingIdentifier</key>
407
+ <string>IF-BETWEEN-UUID</string>
408
+ <key>WFControlFlowMode</key>
409
+ <integer>0</integer>
410
+ <key>WFCondition</key>
411
+ <integer>1003</integer>
412
+ <key>WFInput</key>
413
+ <dict>
414
+ <!-- Type=Variable wrapper around the value being tested -->
415
+ </dict>
416
+ <key>WFNumberValue</key>
417
+ <string>1</string>
418
+ <key>WFAnotherNumber</key>
419
+ <dict>
420
+ <key>Value</key>
421
+ <dict>
422
+ <key>OutputName</key>
423
+ <string>Number</string>
424
+ <key>OutputUUID</key>
425
+ <string>UPPER-BOUND-UUID</string>
426
+ <key>Type</key>
427
+ <string>ActionOutput</string>
428
+ </dict>
429
+ <key>WFSerializationType</key>
430
+ <string>WFTextTokenAttachment</string>
431
+ </dict>
432
+ </dict>
433
+ </dict>
434
+ ```
435
+
436
+ ### Multi-condition If (Any are true / All are true)
437
+
438
+ Apple's modern Shortcuts conditional supports a single If block testing **multiple conditions at once**, joined by either "Any are true" (OR) or "All are true" (AND). This is encoded as `WFConditions` with serialization type `WFContentPredicateTableTemplate`, NOT as a sequence of separate If blocks.
439
+
440
+ **When to use:** the user explicitly asks for "if A or B", "if any of these are true", "if all of these match", or similar combinatorial logic. Otherwise, chain single-condition Ifs.
441
+
442
+ **Top-level structure:** The action sets ONLY `WFConditions`, `WFControlFlowMode`, and `GroupingIdentifier`. There is **no** top-level `WFCondition`, `WFInput`, `WFConditionalActionString`, or `WFNumberValue` — those live inside each template. The validator rejects mixing the two patterns.
443
+
444
+ ```xml
445
+ <dict>
446
+ <key>WFWorkflowActionIdentifier</key>
447
+ <string>is.workflow.actions.conditional</string>
448
+ <key>WFWorkflowActionParameters</key>
449
+ <dict>
450
+ <key>GroupingIdentifier</key>
451
+ <string>MULTI-IF-GROUP-UUID</string>
452
+ <key>WFControlFlowMode</key>
453
+ <integer>0</integer>
454
+ <key>WFConditions</key>
455
+ <dict>
456
+ <key>Value</key>
457
+ <dict>
458
+ <key>WFActionParameterFilterPrefix</key>
459
+ <integer>0</integer>
460
+ <!--
461
+ WFActionParameterFilterPrefix:
462
+ 0 = Any are true (OR — at least one row matches)
463
+ 1 = All are true (AND — every row must match)
464
+ -->
465
+ <key>WFActionParameterFilterTemplates</key>
466
+ <array>
467
+ <!-- Row 1: string is "Yes" -->
468
+ <dict>
469
+ <key>WFCondition</key>
470
+ <integer>4</integer>
471
+ <key>WFConditionalActionString</key>
472
+ <string>Yes</string>
473
+ <key>WFInput</key>
474
+ <dict>
475
+ <key>Type</key>
476
+ <string>Variable</string>
477
+ <key>Variable</key>
478
+ <dict>
479
+ <key>Value</key>
480
+ <dict>
481
+ <key>OutputName</key>
482
+ <string>Text</string>
483
+ <key>OutputUUID</key>
484
+ <string>TEXT-VAR-UUID</string>
485
+ <key>Type</key>
486
+ <string>ActionOutput</string>
487
+ </dict>
488
+ <key>WFSerializationType</key>
489
+ <string>WFTextTokenAttachment</string>
490
+ </dict>
491
+ </dict>
492
+ </dict>
493
+ <!-- Row 2: string is not "No" -->
494
+ <dict>
495
+ <key>WFCondition</key>
496
+ <integer>5</integer>
497
+ <key>WFConditionalActionString</key>
498
+ <string>No</string>
499
+ <key>WFInput</key>
500
+ <dict>
501
+ <!-- Same Type=Variable wrapper -->
502
+ </dict>
503
+ </dict>
504
+ <!-- Row 3: number is greater than 1 -->
505
+ <dict>
506
+ <key>WFCondition</key>
507
+ <integer>2</integer>
508
+ <key>WFNumberValue</key>
509
+ <string>1</string>
510
+ <key>WFInput</key>
511
+ <dict>
512
+ <!-- Type=Variable wrapper pointing at a Number variable -->
513
+ </dict>
514
+ </dict>
515
+ </array>
516
+ <key>WFContentPredicateBoundedDate</key>
517
+ <false/>
518
+ </dict>
519
+ <key>WFSerializationType</key>
520
+ <string>WFContentPredicateTableTemplate</string>
521
+ </dict>
522
+ </dict>
523
+ </dict>
524
+ ```
525
+
526
+ **Per-template rules** (each entry inside `WFActionParameterFilterTemplates`):
527
+
528
+ - Each template is an independent condition with its own `WFCondition`, `WFInput`, and the appropriate literal field (`WFConditionalActionString` for codes 4/5/8/9/99/999, `WFNumberValue` for codes 0/1/2/3, `WFNumberValue`+`WFAnotherNumber` for code 1003, no literal for codes 100/101).
529
+ - Each template's `WFInput` follows the same `Type=Variable` wrapper rule as the single-condition pattern, and can reference a different upstream variable than other templates in the same block.
530
+ - Templates inside the same multi-condition block don't need a `GroupingIdentifier` — the parent action holds it.
531
+ - Apple's table-template format may include `WFContentPredicateBoundedDate` (a boolean) for date-aware filters; emit `<false/>` unless the conditions involve dates.
532
+
533
+ Plain Otherwise / End If actions for a multi-condition block are identical to the single-condition pattern: just `GroupingIdentifier` + `WFControlFlowMode` (1 or 2). A macOS 27+ Otherwise If middle branch can carry condition fields on mode 1.
534
+
535
+ ---
536
+
537
+ ## Choose from Menu
538
+
539
+ Present a menu of options and execute different actions based on the user's choice.
540
+
541
+ ### Structure
542
+ | Mode | Action | Description |
543
+ |------|--------|-------------|
544
+ | 0 | Menu | Define menu with items |
545
+ | 1 | Case | One case per menu item |
546
+ | 2 | End Menu | Close menu |
547
+
548
+ ### Template
549
+
550
+ ```xml
551
+ <!-- Menu Definition -->
552
+ <dict>
553
+ <key>WFWorkflowActionIdentifier</key>
554
+ <string>is.workflow.actions.choosefrommenu</string>
555
+ <key>WFWorkflowActionParameters</key>
556
+ <dict>
557
+ <key>GroupingIdentifier</key>
558
+ <string>MENU-GROUP-UUID</string>
559
+ <key>WFControlFlowMode</key>
560
+ <integer>0</integer>
561
+ <key>WFMenuPrompt</key>
562
+ <string>Choose an option:</string>
563
+ <key>WFMenuItems</key>
564
+ <array>
565
+ <string>Option 1</string>
566
+ <string>Option 2</string>
567
+ <string>Option 3</string>
568
+ </array>
569
+ </dict>
570
+ </dict>
571
+
572
+ <!-- Case 1: Option 1 -->
573
+ <dict>
574
+ <key>WFWorkflowActionIdentifier</key>
575
+ <string>is.workflow.actions.choosefrommenu</string>
576
+ <key>WFWorkflowActionParameters</key>
577
+ <dict>
578
+ <key>GroupingIdentifier</key>
579
+ <string>MENU-GROUP-UUID</string>
580
+ <key>WFControlFlowMode</key>
581
+ <integer>1</integer>
582
+ <key>WFMenuItemTitle</key>
583
+ <string>Option 1</string>
584
+ </dict>
585
+ </dict>
586
+
587
+ <!-- Actions for Option 1 go here -->
588
+
589
+ <!-- Case 2: Option 2 -->
590
+ <dict>
591
+ <key>WFWorkflowActionIdentifier</key>
592
+ <string>is.workflow.actions.choosefrommenu</string>
593
+ <key>WFWorkflowActionParameters</key>
594
+ <dict>
595
+ <key>GroupingIdentifier</key>
596
+ <string>MENU-GROUP-UUID</string>
597
+ <key>WFControlFlowMode</key>
598
+ <integer>1</integer>
599
+ <key>WFMenuItemTitle</key>
600
+ <string>Option 2</string>
601
+ </dict>
602
+ </dict>
603
+
604
+ <!-- Actions for Option 2 go here -->
605
+
606
+ <!-- Case 3: Option 3 -->
607
+ <dict>
608
+ <key>WFWorkflowActionIdentifier</key>
609
+ <string>is.workflow.actions.choosefrommenu</string>
610
+ <key>WFWorkflowActionParameters</key>
611
+ <dict>
612
+ <key>GroupingIdentifier</key>
613
+ <string>MENU-GROUP-UUID</string>
614
+ <key>WFControlFlowMode</key>
615
+ <integer>1</integer>
616
+ <key>WFMenuItemTitle</key>
617
+ <string>Option 3</string>
618
+ </dict>
619
+ </dict>
620
+
621
+ <!-- Actions for Option 3 go here -->
622
+
623
+ <!-- End Menu -->
624
+ <dict>
625
+ <key>WFWorkflowActionIdentifier</key>
626
+ <string>is.workflow.actions.choosefrommenu</string>
627
+ <key>WFWorkflowActionParameters</key>
628
+ <dict>
629
+ <key>GroupingIdentifier</key>
630
+ <string>MENU-GROUP-UUID</string>
631
+ <key>WFControlFlowMode</key>
632
+ <integer>2</integer>
633
+ </dict>
634
+ </dict>
635
+ ```
636
+
637
+ ### Important Notes (from 127 real shortcuts analysis)
638
+
639
+ 1. **Menu definition mode 0**: Uses `WFMenuItems` array containing all option strings
640
+ 2. **Case mode 1 for each option**: One mode 1 action per menu item with exact `WFMenuItemTitle` match
641
+ 3. **WFMenuItemTitle must match exactly** - Each case title must match the corresponding item in WFMenuItems exactly (case-sensitive)
642
+ 4. **Order must match** - Case (mode 1) actions must appear in the same order as items in the WFMenuItems array
643
+ 5. **One case per item** - You need exactly one mode 1 action for each menu item
644
+ 6. **Close with mode 2** - End the menu structure with a mode 2 action (uses same GroupingIdentifier, no additional parameters)
645
+
646
+ ---
647
+
648
+ ## Nesting Control Flow
649
+
650
+ Control flow blocks can be nested to arbitrary depth. Analysis of 127 real shortcuts confirms nesting up to **depth 7** in production shortcuts (AppRedirect.xml).
651
+
652
+ **Key rule:** Each nested block needs its own unique GroupingIdentifier:
653
+
654
+ ```xml
655
+ <!-- Outer Repeat -->
656
+ <dict>
657
+ <key>WFWorkflowActionIdentifier</key>
658
+ <string>is.workflow.actions.repeat.count</string>
659
+ <key>WFWorkflowActionParameters</key>
660
+ <dict>
661
+ <key>GroupingIdentifier</key>
662
+ <string>OUTER-LOOP-UUID</string>
663
+ <key>WFControlFlowMode</key>
664
+ <integer>0</integer>
665
+ <key>WFRepeatCount</key>
666
+ <integer>3</integer>
667
+ </dict>
668
+ </dict>
669
+
670
+ <!-- Inner Conditional -->
671
+ <dict>
672
+ <key>WFWorkflowActionIdentifier</key>
673
+ <string>is.workflow.actions.conditional</string>
674
+ <key>WFWorkflowActionParameters</key>
675
+ <dict>
676
+ <key>GroupingIdentifier</key>
677
+ <string>INNER-IF-UUID</string>
678
+ <key>WFControlFlowMode</key>
679
+ <integer>0</integer>
680
+ <!-- condition params -->
681
+ </dict>
682
+ </dict>
683
+
684
+ <!-- Inner Otherwise -->
685
+ <dict>
686
+ <key>WFWorkflowActionIdentifier</key>
687
+ <string>is.workflow.actions.conditional</string>
688
+ <key>WFWorkflowActionParameters</key>
689
+ <dict>
690
+ <key>GroupingIdentifier</key>
691
+ <string>INNER-IF-UUID</string>
692
+ <key>WFControlFlowMode</key>
693
+ <integer>1</integer>
694
+ </dict>
695
+ </dict>
696
+
697
+ <!-- Inner End If -->
698
+ <dict>
699
+ <key>WFWorkflowActionIdentifier</key>
700
+ <string>is.workflow.actions.conditional</string>
701
+ <key>WFWorkflowActionParameters</key>
702
+ <dict>
703
+ <key>GroupingIdentifier</key>
704
+ <string>INNER-IF-UUID</string>
705
+ <key>WFControlFlowMode</key>
706
+ <integer>2</integer>
707
+ </dict>
708
+ </dict>
709
+
710
+ <!-- Outer End Repeat -->
711
+ <dict>
712
+ <key>WFWorkflowActionIdentifier</key>
713
+ <string>is.workflow.actions.repeat.count</string>
714
+ <key>WFWorkflowActionParameters</key>
715
+ <dict>
716
+ <key>GroupingIdentifier</key>
717
+ <string>OUTER-LOOP-UUID</string>
718
+ <key>WFControlFlowMode</key>
719
+ <integer>2</integer>
720
+ </dict>
721
+ </dict>
722
+ ```
723
+
724
+ ---
725
+
726
+ ## Nothing and Exit Actions
727
+
728
+ Two special control flow actions have no parameters:
729
+
730
+ - **Nothing** (`is.workflow.actions.nothing`): Produces no output. Use as a placeholder inside branches that should intentionally return no result.
731
+ - **Exit Shortcut** (`is.workflow.actions.exit`): Immediately stops the shortcut. Empty parameters dict.
732
+
733
+ Both need only `WFWorkflowActionIdentifier` and an empty `WFWorkflowActionParameters` dict (no UUID needed).
734
+
735
+ ---
736
+
737
+ ## Common Mistakes
738
+
739
+ 1. **Using string instead of integer for WFControlFlowMode**
740
+ - Wrong: `<string>0</string>`
741
+ - Right: `<integer>0</integer>`
742
+
743
+ 2. **Mismatched GroupingIdentifier**
744
+ - All parts of a control flow block must share the same GroupingIdentifier
745
+ - Nested blocks MUST have distinct GroupingIdentifiers per level
746
+ - Confirmed working up to depth 7 (from AppRedirect.xml in 127-shortcut analysis)
747
+
748
+ 3. **Missing End action**
749
+ - Every start (mode 0) must have a corresponding end (mode 2)
750
+
751
+ 4. **Wrong order in menu cases**
752
+ - Cases must appear in the same order as WFMenuItems
753
+
754
+ 5. **Referencing wrong UUID for loop items**
755
+ - Repeat Item uses the **start** action's UUID (OR `Type=Variable` with `VariableName="Repeat Item"` — preferred)
756
+ - Repeat Index uses `Type=Variable` with `VariableName="Repeat Index"` — NOT ActionOutput
757
+
758
+ 6. **Using WFTextTokenAttachment for display parameters in control flow branches**
759
+ - Show Alert message, Notification body, and Show Result text MUST use `WFTextTokenString` (with `` placeholder + `attachmentsByRange`)
760
+ - Using `WFTextTokenAttachment` for these causes default/empty text at runtime
761
+
762
+ 7. **Omitting `WFInput` on conditional codes 0–3**
763
+ - All conditional codes — numeric, string, and existence — require an explicit `WFInput` as a `Type=Variable` wrapper. The earlier "implicit input for numeric conditions" rule was wrong; an Apple-built sample shortcut has explicit `WFInput` on every conditional, including codes 0, 1, 2, 3, and 1003.
764
+
765
+ 8. **Confusing condition code semantics**
766
+ - Code `0` = `is less than` (NOT equals). Code `1` = `is less than or equal to`. Code `2` = `is greater than`. Code `3` = `is greater than or equal to`.
767
+ - Code `4` = string `is` (exact equality). Code `99` = string `contains` (substring). Using code 4 when you mean "contains" will only match the exact string.
768
+
769
+ 9. **Mixing single-condition and multi-condition fields on one If**
770
+ - If you set `WFConditions` (multi-condition `WFContentPredicateTableTemplate`), do NOT also set top-level `WFCondition`, `WFInput`, or any literal field. The validator rejects this.
771
+ - Conversely, if you set top-level `WFCondition` + `WFInput`, do NOT set `WFConditions`. Pick one pattern per If.
772
+
773
+ 10. **Forgetting `WFAnotherNumber` on `is between` (code 1003)**
774
+ - Code 1003 needs both `WFNumberValue` (lower bound, literal string) and `WFAnotherNumber` (upper bound, token attachment). Missing `WFAnotherNumber` imports as an empty upper-bound field.
775
+
776
+ 11. **Treating Otherwise If as a separate action**
777
+ - macOS 27 serializes Otherwise If as `is.workflow.actions.conditional` with `WFControlFlowMode = 1` plus condition fields. Plain Otherwise is the same mode with no condition fields.