@particle-academy/fancy-conformance 0.5.0 → 0.6.0

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.
@@ -0,0 +1,1855 @@
1
+ {
2
+ "$schema": "../../../schema/case-table.schema.json",
3
+ "suite": "flow/graph-runs",
4
+ "cases": [
5
+ {
6
+ "id": "0001-manual-transform-output",
7
+ "title": "Manual trigger seeds a payload; transform extracts a field; output captures it.",
8
+ "since": "0.6.0",
9
+ "tags": [
10
+ "trigger",
11
+ "logic",
12
+ "output"
13
+ ],
14
+ "input": {
15
+ "schema": {
16
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
17
+ "version": 1,
18
+ "graph": {
19
+ "nodes": [
20
+ {
21
+ "id": "t",
22
+ "kind": "manual_trigger",
23
+ "position": {
24
+ "x": 0,
25
+ "y": 0
26
+ }
27
+ },
28
+ {
29
+ "id": "tf",
30
+ "kind": "transform",
31
+ "position": {
32
+ "x": 1,
33
+ "y": 0
34
+ },
35
+ "config": {
36
+ "expression": "{{ $json.user.name }}"
37
+ }
38
+ },
39
+ {
40
+ "id": "o",
41
+ "kind": "output",
42
+ "position": {
43
+ "x": 2,
44
+ "y": 0
45
+ }
46
+ }
47
+ ],
48
+ "edges": [
49
+ {
50
+ "id": "e1",
51
+ "source": "t",
52
+ "target": "tf"
53
+ },
54
+ {
55
+ "id": "e2",
56
+ "source": "tf",
57
+ "target": "o"
58
+ }
59
+ ]
60
+ }
61
+ },
62
+ "initialInputs": {
63
+ "t": {
64
+ "user": {
65
+ "name": "Ada"
66
+ }
67
+ }
68
+ }
69
+ },
70
+ "expected": {
71
+ "ok": true,
72
+ "outputs": {
73
+ "t": {
74
+ "user": {
75
+ "name": "Ada"
76
+ }
77
+ },
78
+ "tf": "Ada",
79
+ "o": "Ada"
80
+ }
81
+ }
82
+ },
83
+ {
84
+ "id": "0002-webhook-branch-true-notify",
85
+ "title": "Webhook payload branches true and notifies; the merge point is the output.",
86
+ "since": "0.6.0",
87
+ "tags": [
88
+ "trigger",
89
+ "branch",
90
+ "merge",
91
+ "human"
92
+ ],
93
+ "input": {
94
+ "schema": {
95
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
96
+ "version": 1,
97
+ "graph": {
98
+ "nodes": [
99
+ {
100
+ "id": "wh",
101
+ "kind": "webhook_trigger",
102
+ "position": {
103
+ "x": 0,
104
+ "y": 0
105
+ }
106
+ },
107
+ {
108
+ "id": "br",
109
+ "kind": "branch",
110
+ "position": {
111
+ "x": 1,
112
+ "y": 0
113
+ },
114
+ "config": {
115
+ "condition": "{{ $json.vip }}"
116
+ }
117
+ },
118
+ {
119
+ "id": "nf",
120
+ "kind": "notify",
121
+ "position": {
122
+ "x": 2,
123
+ "y": 0
124
+ },
125
+ "config": {
126
+ "channel": "slack",
127
+ "to": "#vips",
128
+ "message": "VIP {{ $json.name }}"
129
+ }
130
+ },
131
+ {
132
+ "id": "o",
133
+ "kind": "output",
134
+ "position": {
135
+ "x": 3,
136
+ "y": 0
137
+ }
138
+ }
139
+ ],
140
+ "edges": [
141
+ {
142
+ "id": "e1",
143
+ "source": "wh",
144
+ "target": "br"
145
+ },
146
+ {
147
+ "id": "e2",
148
+ "source": "br",
149
+ "target": "nf",
150
+ "sourceHandle": "true"
151
+ },
152
+ {
153
+ "id": "e3",
154
+ "source": "nf",
155
+ "target": "o"
156
+ }
157
+ ]
158
+ }
159
+ },
160
+ "initialInputs": {
161
+ "wh": {
162
+ "payload": {
163
+ "vip": true,
164
+ "name": "Ada"
165
+ }
166
+ }
167
+ }
168
+ },
169
+ "expected": {
170
+ "ok": true,
171
+ "outputs": {
172
+ "wh": {
173
+ "vip": true,
174
+ "name": "Ada"
175
+ },
176
+ "br": {
177
+ "branch": "true",
178
+ "value": {
179
+ "vip": true,
180
+ "name": "Ada"
181
+ }
182
+ },
183
+ "nf": {
184
+ "sent": true,
185
+ "channel": "slack",
186
+ "to": "#vips",
187
+ "message": "VIP Ada"
188
+ },
189
+ "o": {
190
+ "sent": true,
191
+ "channel": "slack",
192
+ "to": "#vips",
193
+ "message": "VIP Ada"
194
+ }
195
+ }
196
+ }
197
+ },
198
+ {
199
+ "id": "0003-branch-false-log",
200
+ "title": "A false condition routes to the log branch.",
201
+ "since": "0.6.0",
202
+ "tags": [
203
+ "branch",
204
+ "output"
205
+ ],
206
+ "input": {
207
+ "schema": {
208
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
209
+ "version": 1,
210
+ "graph": {
211
+ "nodes": [
212
+ {
213
+ "id": "t",
214
+ "kind": "manual_trigger",
215
+ "position": {
216
+ "x": 0,
217
+ "y": 0
218
+ }
219
+ },
220
+ {
221
+ "id": "br",
222
+ "kind": "branch",
223
+ "position": {
224
+ "x": 1,
225
+ "y": 0
226
+ },
227
+ "config": {
228
+ "condition": "{{ $json.vip }}"
229
+ }
230
+ },
231
+ {
232
+ "id": "lg",
233
+ "kind": "log",
234
+ "position": {
235
+ "x": 2,
236
+ "y": 0
237
+ },
238
+ "config": {
239
+ "level": "info",
240
+ "message": "regular {{ $json.name }}"
241
+ }
242
+ },
243
+ {
244
+ "id": "o",
245
+ "kind": "output",
246
+ "position": {
247
+ "x": 3,
248
+ "y": 0
249
+ }
250
+ }
251
+ ],
252
+ "edges": [
253
+ {
254
+ "id": "e1",
255
+ "source": "t",
256
+ "target": "br"
257
+ },
258
+ {
259
+ "id": "e2",
260
+ "source": "br",
261
+ "target": "lg",
262
+ "sourceHandle": "false"
263
+ },
264
+ {
265
+ "id": "e3",
266
+ "source": "lg",
267
+ "target": "o"
268
+ }
269
+ ]
270
+ }
271
+ },
272
+ "initialInputs": {
273
+ "t": {
274
+ "vip": false,
275
+ "name": "Bo"
276
+ }
277
+ }
278
+ },
279
+ "expected": {
280
+ "ok": true,
281
+ "outputs": {
282
+ "t": {
283
+ "vip": false,
284
+ "name": "Bo"
285
+ },
286
+ "br": {
287
+ "branch": "false",
288
+ "value": {
289
+ "vip": false,
290
+ "name": "Bo"
291
+ }
292
+ },
293
+ "lg": {
294
+ "logged": "regular Bo",
295
+ "level": "info"
296
+ },
297
+ "o": {
298
+ "logged": "regular Bo",
299
+ "level": "info"
300
+ }
301
+ }
302
+ }
303
+ },
304
+ {
305
+ "id": "0004-switch-case",
306
+ "title": "Switch routes on a key to case_b.",
307
+ "since": "0.6.0",
308
+ "tags": [
309
+ "branch",
310
+ "ports"
311
+ ],
312
+ "input": {
313
+ "schema": {
314
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
315
+ "version": 1,
316
+ "graph": {
317
+ "nodes": [
318
+ {
319
+ "id": "t",
320
+ "kind": "manual_trigger",
321
+ "position": {
322
+ "x": 0,
323
+ "y": 0
324
+ }
325
+ },
326
+ {
327
+ "id": "sw",
328
+ "kind": "switch_case",
329
+ "position": {
330
+ "x": 1,
331
+ "y": 0
332
+ },
333
+ "config": {
334
+ "value": "{{ $json.kind }}",
335
+ "cases": {
336
+ "a": "case_a",
337
+ "b": "case_b"
338
+ }
339
+ }
340
+ },
341
+ {
342
+ "id": "o",
343
+ "kind": "output",
344
+ "position": {
345
+ "x": 2,
346
+ "y": 0
347
+ }
348
+ }
349
+ ],
350
+ "edges": [
351
+ {
352
+ "id": "e1",
353
+ "source": "t",
354
+ "target": "sw"
355
+ },
356
+ {
357
+ "id": "e2",
358
+ "source": "sw",
359
+ "target": "o",
360
+ "sourceHandle": "case_b"
361
+ }
362
+ ]
363
+ }
364
+ },
365
+ "initialInputs": {
366
+ "t": {
367
+ "kind": "b"
368
+ }
369
+ }
370
+ },
371
+ "expected": {
372
+ "ok": true,
373
+ "outputs": {
374
+ "t": {
375
+ "kind": "b"
376
+ },
377
+ "sw": {
378
+ "__port": "case_b",
379
+ "value": {
380
+ "kind": "b"
381
+ }
382
+ },
383
+ "o": {
384
+ "kind": "b"
385
+ }
386
+ }
387
+ }
388
+ },
389
+ {
390
+ "id": "0005-merge-after-decision",
391
+ "title": "A decision routes down one branch; the merge point still runs (#1).",
392
+ "since": "0.6.0",
393
+ "tags": [
394
+ "branch",
395
+ "merge",
396
+ "hazard"
397
+ ],
398
+ "input": {
399
+ "schema": {
400
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
401
+ "version": 1,
402
+ "graph": {
403
+ "nodes": [
404
+ {
405
+ "id": "t",
406
+ "kind": "manual_trigger",
407
+ "position": {
408
+ "x": 0,
409
+ "y": 0
410
+ }
411
+ },
412
+ {
413
+ "id": "br",
414
+ "kind": "branch",
415
+ "position": {
416
+ "x": 1,
417
+ "y": 0
418
+ },
419
+ "config": {
420
+ "condition": "{{ $json.ok }}"
421
+ }
422
+ },
423
+ {
424
+ "id": "a",
425
+ "kind": "transform",
426
+ "position": {
427
+ "x": 2,
428
+ "y": 0
429
+ },
430
+ "config": {
431
+ "expression": "A"
432
+ }
433
+ },
434
+ {
435
+ "id": "b",
436
+ "kind": "transform",
437
+ "position": {
438
+ "x": 2,
439
+ "y": 1
440
+ },
441
+ "config": {
442
+ "expression": "B"
443
+ }
444
+ },
445
+ {
446
+ "id": "m",
447
+ "kind": "merge",
448
+ "position": {
449
+ "x": 3,
450
+ "y": 0
451
+ },
452
+ "config": {
453
+ "mode": "merge"
454
+ }
455
+ },
456
+ {
457
+ "id": "o",
458
+ "kind": "output",
459
+ "position": {
460
+ "x": 4,
461
+ "y": 0
462
+ }
463
+ }
464
+ ],
465
+ "edges": [
466
+ {
467
+ "id": "e1",
468
+ "source": "t",
469
+ "target": "br"
470
+ },
471
+ {
472
+ "id": "e2",
473
+ "source": "br",
474
+ "target": "a",
475
+ "sourceHandle": "true"
476
+ },
477
+ {
478
+ "id": "e3",
479
+ "source": "br",
480
+ "target": "b",
481
+ "sourceHandle": "false"
482
+ },
483
+ {
484
+ "id": "e4",
485
+ "source": "a",
486
+ "target": "m",
487
+ "targetHandle": "a"
488
+ },
489
+ {
490
+ "id": "e5",
491
+ "source": "b",
492
+ "target": "m",
493
+ "targetHandle": "b"
494
+ },
495
+ {
496
+ "id": "e6",
497
+ "source": "m",
498
+ "target": "o"
499
+ }
500
+ ]
501
+ }
502
+ },
503
+ "initialInputs": {
504
+ "t": {
505
+ "ok": true
506
+ }
507
+ }
508
+ },
509
+ "expected": {
510
+ "ok": true,
511
+ "outputs": {
512
+ "t": {
513
+ "ok": true
514
+ },
515
+ "br": {
516
+ "branch": "true",
517
+ "value": {
518
+ "ok": true
519
+ }
520
+ },
521
+ "a": "A",
522
+ "m": {
523
+ "a": "A"
524
+ },
525
+ "o": {
526
+ "a": "A"
527
+ }
528
+ }
529
+ },
530
+ "notes": "The merge-after-decision case (#1). A node runs when AT LEAST ONE incoming edge is active, never when all are: a Decision leaves the untaken branch's edge dead forever, so an `every`-style check skips the shared continuation and halts the run after the first branch. An implementation requiring all incoming edges fails exactly this case and 0002."
531
+ },
532
+ {
533
+ "id": "0006-merge-concat",
534
+ "title": "Two lists concatenated.",
535
+ "since": "0.6.0",
536
+ "tags": [
537
+ "merge"
538
+ ],
539
+ "input": {
540
+ "schema": {
541
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
542
+ "version": 1,
543
+ "graph": {
544
+ "nodes": [
545
+ {
546
+ "id": "a",
547
+ "kind": "variable",
548
+ "position": {
549
+ "x": 0,
550
+ "y": 0
551
+ },
552
+ "config": {
553
+ "name": "a",
554
+ "value": "{{ $json.left }}"
555
+ }
556
+ },
557
+ {
558
+ "id": "b",
559
+ "kind": "variable",
560
+ "position": {
561
+ "x": 0,
562
+ "y": 1
563
+ },
564
+ "config": {
565
+ "name": "b",
566
+ "value": "{{ $json.right }}"
567
+ }
568
+ },
569
+ {
570
+ "id": "m",
571
+ "kind": "merge",
572
+ "position": {
573
+ "x": 1,
574
+ "y": 0
575
+ },
576
+ "config": {
577
+ "mode": "concat"
578
+ }
579
+ }
580
+ ],
581
+ "edges": [
582
+ {
583
+ "id": "e1",
584
+ "source": "a",
585
+ "target": "m",
586
+ "targetHandle": "a"
587
+ },
588
+ {
589
+ "id": "e2",
590
+ "source": "b",
591
+ "target": "m",
592
+ "targetHandle": "b"
593
+ }
594
+ ]
595
+ }
596
+ },
597
+ "initialInputs": {
598
+ "a": {
599
+ "left": [
600
+ 1,
601
+ 2
602
+ ]
603
+ },
604
+ "b": {
605
+ "right": [
606
+ 3
607
+ ]
608
+ }
609
+ }
610
+ },
611
+ "expected": {
612
+ "ok": true,
613
+ "outputs": {
614
+ "a": [
615
+ 1,
616
+ 2
617
+ ],
618
+ "b": [
619
+ 3
620
+ ],
621
+ "m": [
622
+ 1,
623
+ 2,
624
+ 3
625
+ ]
626
+ }
627
+ }
628
+ },
629
+ {
630
+ "id": "0007-for-each",
631
+ "title": "Resolve a list from the payload.",
632
+ "since": "0.6.0",
633
+ "tags": [
634
+ "logic"
635
+ ],
636
+ "input": {
637
+ "schema": {
638
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
639
+ "version": 1,
640
+ "graph": {
641
+ "nodes": [
642
+ {
643
+ "id": "t",
644
+ "kind": "manual_trigger",
645
+ "position": {
646
+ "x": 0,
647
+ "y": 0
648
+ }
649
+ },
650
+ {
651
+ "id": "fe",
652
+ "kind": "for_each",
653
+ "position": {
654
+ "x": 1,
655
+ "y": 0
656
+ },
657
+ "config": {
658
+ "source": "{{ $json.items }}"
659
+ }
660
+ }
661
+ ],
662
+ "edges": [
663
+ {
664
+ "id": "e1",
665
+ "source": "t",
666
+ "target": "fe"
667
+ }
668
+ ]
669
+ }
670
+ },
671
+ "initialInputs": {
672
+ "t": {
673
+ "items": [
674
+ "x",
675
+ "y",
676
+ "z"
677
+ ]
678
+ }
679
+ }
680
+ },
681
+ "expected": {
682
+ "ok": true,
683
+ "outputs": {
684
+ "t": {
685
+ "items": [
686
+ "x",
687
+ "y",
688
+ "z"
689
+ ]
690
+ },
691
+ "fe": {
692
+ "items": [
693
+ "x",
694
+ "y",
695
+ "z"
696
+ ],
697
+ "count": 3
698
+ }
699
+ }
700
+ }
701
+ },
702
+ {
703
+ "id": "0008-memory-store",
704
+ "title": "Write to memory then read it back.",
705
+ "since": "0.6.0",
706
+ "tags": [
707
+ "data"
708
+ ],
709
+ "input": {
710
+ "schema": {
711
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
712
+ "version": 1,
713
+ "graph": {
714
+ "nodes": [
715
+ {
716
+ "id": "t",
717
+ "kind": "manual_trigger",
718
+ "position": {
719
+ "x": 0,
720
+ "y": 0
721
+ }
722
+ },
723
+ {
724
+ "id": "w",
725
+ "kind": "memory_store",
726
+ "position": {
727
+ "x": 1,
728
+ "y": 0
729
+ },
730
+ "config": {
731
+ "operation": "write",
732
+ "key": "pref",
733
+ "value": "{{ $json.pref }}"
734
+ }
735
+ },
736
+ {
737
+ "id": "r",
738
+ "kind": "memory_store",
739
+ "position": {
740
+ "x": 2,
741
+ "y": 0
742
+ },
743
+ "config": {
744
+ "operation": "read",
745
+ "key": "pref"
746
+ }
747
+ }
748
+ ],
749
+ "edges": [
750
+ {
751
+ "id": "e1",
752
+ "source": "t",
753
+ "target": "w"
754
+ },
755
+ {
756
+ "id": "e2",
757
+ "source": "w",
758
+ "target": "r"
759
+ }
760
+ ]
761
+ }
762
+ },
763
+ "initialInputs": {
764
+ "t": {
765
+ "pref": "dark"
766
+ }
767
+ }
768
+ },
769
+ "expected": {
770
+ "ok": true,
771
+ "outputs": {
772
+ "t": {
773
+ "pref": "dark"
774
+ },
775
+ "w": "dark",
776
+ "r": "dark"
777
+ }
778
+ }
779
+ },
780
+ {
781
+ "id": "0009-data-store",
782
+ "title": "Set a row then query the table.",
783
+ "since": "0.6.0",
784
+ "tags": [
785
+ "data"
786
+ ],
787
+ "input": {
788
+ "schema": {
789
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
790
+ "version": 1,
791
+ "graph": {
792
+ "nodes": [
793
+ {
794
+ "id": "t",
795
+ "kind": "manual_trigger",
796
+ "position": {
797
+ "x": 0,
798
+ "y": 0
799
+ }
800
+ },
801
+ {
802
+ "id": "s",
803
+ "kind": "data_store",
804
+ "position": {
805
+ "x": 1,
806
+ "y": 0
807
+ },
808
+ "config": {
809
+ "operation": "set",
810
+ "table": "users",
811
+ "key": "1",
812
+ "value": "{{ $json.row }}"
813
+ }
814
+ },
815
+ {
816
+ "id": "q",
817
+ "kind": "data_store",
818
+ "position": {
819
+ "x": 2,
820
+ "y": 0
821
+ },
822
+ "config": {
823
+ "operation": "list",
824
+ "table": "users"
825
+ }
826
+ }
827
+ ],
828
+ "edges": [
829
+ {
830
+ "id": "e1",
831
+ "source": "t",
832
+ "target": "s"
833
+ },
834
+ {
835
+ "id": "e2",
836
+ "source": "s",
837
+ "target": "q"
838
+ }
839
+ ]
840
+ }
841
+ },
842
+ "initialInputs": {
843
+ "t": {
844
+ "row": {
845
+ "name": "A"
846
+ }
847
+ }
848
+ }
849
+ },
850
+ "expected": {
851
+ "ok": true,
852
+ "outputs": {
853
+ "t": {
854
+ "row": {
855
+ "name": "A"
856
+ }
857
+ },
858
+ "s": {
859
+ "name": "A"
860
+ },
861
+ "q": {
862
+ "1": {
863
+ "name": "A"
864
+ }
865
+ }
866
+ }
867
+ }
868
+ },
869
+ {
870
+ "id": "0010-variable-transform",
871
+ "title": "A variable feeds a transform.",
872
+ "since": "0.6.0",
873
+ "tags": [
874
+ "data",
875
+ "logic"
876
+ ],
877
+ "input": {
878
+ "schema": {
879
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
880
+ "version": 1,
881
+ "graph": {
882
+ "nodes": [
883
+ {
884
+ "id": "v",
885
+ "kind": "variable",
886
+ "position": {
887
+ "x": 0,
888
+ "y": 0
889
+ },
890
+ "config": {
891
+ "name": "who",
892
+ "value": "{{ $json.who }}"
893
+ }
894
+ },
895
+ {
896
+ "id": "tf",
897
+ "kind": "transform",
898
+ "position": {
899
+ "x": 1,
900
+ "y": 0
901
+ },
902
+ "config": {
903
+ "expression": "Hello {{ $json }}"
904
+ }
905
+ }
906
+ ],
907
+ "edges": [
908
+ {
909
+ "id": "e1",
910
+ "source": "v",
911
+ "target": "tf"
912
+ }
913
+ ]
914
+ }
915
+ },
916
+ "initialInputs": {
917
+ "v": {
918
+ "who": "world"
919
+ }
920
+ }
921
+ },
922
+ "expected": {
923
+ "ok": true,
924
+ "outputs": {
925
+ "v": "world",
926
+ "tf": "Hello world"
927
+ }
928
+ }
929
+ },
930
+ {
931
+ "id": "0011-llm-call",
932
+ "title": "Deterministic echo LLM completion.",
933
+ "since": "0.6.0",
934
+ "tags": [
935
+ "ai"
936
+ ],
937
+ "input": {
938
+ "schema": {
939
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
940
+ "version": 1,
941
+ "graph": {
942
+ "nodes": [
943
+ {
944
+ "id": "t",
945
+ "kind": "manual_trigger",
946
+ "position": {
947
+ "x": 0,
948
+ "y": 0
949
+ }
950
+ },
951
+ {
952
+ "id": "llm",
953
+ "kind": "llm_call",
954
+ "position": {
955
+ "x": 1,
956
+ "y": 0
957
+ },
958
+ "config": {
959
+ "model": "claude-sonnet-4-5",
960
+ "prompt": "{{ $json.q }}"
961
+ }
962
+ }
963
+ ],
964
+ "edges": [
965
+ {
966
+ "id": "e1",
967
+ "source": "t",
968
+ "target": "llm"
969
+ }
970
+ ]
971
+ }
972
+ },
973
+ "initialInputs": {
974
+ "t": {
975
+ "q": "why is the sky blue"
976
+ }
977
+ }
978
+ },
979
+ "expected": {
980
+ "ok": true,
981
+ "outputs": {
982
+ "t": {
983
+ "q": "why is the sky blue"
984
+ },
985
+ "llm": {
986
+ "text": "[claude-sonnet-4-5] why is the sky blue",
987
+ "usage": {
988
+ "input_tokens": 5,
989
+ "output_tokens": 5
990
+ }
991
+ }
992
+ }
993
+ }
994
+ },
995
+ {
996
+ "id": "0012-tool-use",
997
+ "title": "Invoke a named tool with resolved args.",
998
+ "since": "0.6.0",
999
+ "tags": [
1000
+ "ai"
1001
+ ],
1002
+ "input": {
1003
+ "schema": {
1004
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
1005
+ "version": 1,
1006
+ "graph": {
1007
+ "nodes": [
1008
+ {
1009
+ "id": "t",
1010
+ "kind": "manual_trigger",
1011
+ "position": {
1012
+ "x": 0,
1013
+ "y": 0
1014
+ }
1015
+ },
1016
+ {
1017
+ "id": "tu",
1018
+ "kind": "tool_use",
1019
+ "position": {
1020
+ "x": 1,
1021
+ "y": 0
1022
+ },
1023
+ "config": {
1024
+ "tool": "search_index",
1025
+ "args": "{{ $json }}"
1026
+ }
1027
+ }
1028
+ ],
1029
+ "edges": [
1030
+ {
1031
+ "id": "e1",
1032
+ "source": "t",
1033
+ "target": "tu"
1034
+ }
1035
+ ]
1036
+ }
1037
+ },
1038
+ "initialInputs": {
1039
+ "t": {
1040
+ "query": "flows"
1041
+ }
1042
+ }
1043
+ },
1044
+ "expected": {
1045
+ "ok": true,
1046
+ "outputs": {
1047
+ "t": {
1048
+ "query": "flows"
1049
+ },
1050
+ "tu": {
1051
+ "tool": "search_index",
1052
+ "args": {
1053
+ "query": "flows"
1054
+ }
1055
+ }
1056
+ }
1057
+ }
1058
+ },
1059
+ {
1060
+ "id": "0013-embed-search",
1061
+ "title": "Embed a query against an empty vector store.",
1062
+ "since": "0.6.0",
1063
+ "tags": [
1064
+ "ai"
1065
+ ],
1066
+ "input": {
1067
+ "schema": {
1068
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
1069
+ "version": 1,
1070
+ "graph": {
1071
+ "nodes": [
1072
+ {
1073
+ "id": "t",
1074
+ "kind": "manual_trigger",
1075
+ "position": {
1076
+ "x": 0,
1077
+ "y": 0
1078
+ }
1079
+ },
1080
+ {
1081
+ "id": "es",
1082
+ "kind": "embed_search",
1083
+ "position": {
1084
+ "x": 1,
1085
+ "y": 0
1086
+ },
1087
+ "config": {
1088
+ "query": "{{ $json.q }}",
1089
+ "topK": 3
1090
+ }
1091
+ }
1092
+ ],
1093
+ "edges": [
1094
+ {
1095
+ "id": "e1",
1096
+ "source": "t",
1097
+ "target": "es"
1098
+ }
1099
+ ]
1100
+ }
1101
+ },
1102
+ "initialInputs": {
1103
+ "t": {
1104
+ "q": "onboarding"
1105
+ }
1106
+ }
1107
+ },
1108
+ "expected": {
1109
+ "ok": true,
1110
+ "outputs": {
1111
+ "t": {
1112
+ "q": "onboarding"
1113
+ },
1114
+ "es": {
1115
+ "query": "onboarding",
1116
+ "matches": []
1117
+ }
1118
+ }
1119
+ }
1120
+ },
1121
+ {
1122
+ "id": "0014-api-request",
1123
+ "title": "HTTP GET with an interpolated URL (echo client).",
1124
+ "since": "0.6.0",
1125
+ "tags": [
1126
+ "io"
1127
+ ],
1128
+ "input": {
1129
+ "schema": {
1130
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
1131
+ "version": 1,
1132
+ "graph": {
1133
+ "nodes": [
1134
+ {
1135
+ "id": "t",
1136
+ "kind": "manual_trigger",
1137
+ "position": {
1138
+ "x": 0,
1139
+ "y": 0
1140
+ }
1141
+ },
1142
+ {
1143
+ "id": "api",
1144
+ "kind": "api_request",
1145
+ "position": {
1146
+ "x": 1,
1147
+ "y": 0
1148
+ },
1149
+ "config": {
1150
+ "method": "GET",
1151
+ "url": "https://api.example.com/users/{{ $json.id }}"
1152
+ }
1153
+ }
1154
+ ],
1155
+ "edges": [
1156
+ {
1157
+ "id": "e1",
1158
+ "source": "t",
1159
+ "target": "api"
1160
+ }
1161
+ ]
1162
+ }
1163
+ },
1164
+ "initialInputs": {
1165
+ "t": {
1166
+ "id": 42
1167
+ }
1168
+ }
1169
+ },
1170
+ "expected": {
1171
+ "ok": true,
1172
+ "outputs": {
1173
+ "t": {
1174
+ "id": 42
1175
+ },
1176
+ "api": {
1177
+ "status": 200,
1178
+ "headers": {},
1179
+ "body": {
1180
+ "echoed": {
1181
+ "method": "GET",
1182
+ "url": "https://api.example.com/users/42",
1183
+ "body": null
1184
+ }
1185
+ }
1186
+ }
1187
+ }
1188
+ },
1189
+ "notes": "`headers` is an empty MAP, not an empty list. The PHP golden this was promoted from baked `[]` because PHP cannot distinguish the two when encoding; that recorded the encoding rather than the value, and cost the Python port a normaliser."
1190
+ },
1191
+ {
1192
+ "id": "0015-webhook-out",
1193
+ "title": "POST a payload to a URL (echo client).",
1194
+ "since": "0.6.0",
1195
+ "tags": [
1196
+ "io"
1197
+ ],
1198
+ "input": {
1199
+ "schema": {
1200
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
1201
+ "version": 1,
1202
+ "graph": {
1203
+ "nodes": [
1204
+ {
1205
+ "id": "t",
1206
+ "kind": "manual_trigger",
1207
+ "position": {
1208
+ "x": 0,
1209
+ "y": 0
1210
+ }
1211
+ },
1212
+ {
1213
+ "id": "wo",
1214
+ "kind": "webhook_out",
1215
+ "position": {
1216
+ "x": 1,
1217
+ "y": 0
1218
+ },
1219
+ "config": {
1220
+ "url": "https://hooks.example.com/x",
1221
+ "payload": "{{ $json }}"
1222
+ }
1223
+ }
1224
+ ],
1225
+ "edges": [
1226
+ {
1227
+ "id": "e1",
1228
+ "source": "t",
1229
+ "target": "wo"
1230
+ }
1231
+ ]
1232
+ }
1233
+ },
1234
+ "initialInputs": {
1235
+ "t": {
1236
+ "event": "signup"
1237
+ }
1238
+ }
1239
+ },
1240
+ "expected": {
1241
+ "ok": true,
1242
+ "outputs": {
1243
+ "t": {
1244
+ "event": "signup"
1245
+ },
1246
+ "wo": {
1247
+ "sent": true,
1248
+ "status": 200,
1249
+ "response": {
1250
+ "echoed": {
1251
+ "method": "POST",
1252
+ "url": "https://hooks.example.com/x",
1253
+ "body": {
1254
+ "event": "signup"
1255
+ }
1256
+ }
1257
+ }
1258
+ }
1259
+ }
1260
+ }
1261
+ },
1262
+ {
1263
+ "id": "0016-schedule-trigger",
1264
+ "title": "A cron-scheduled entry point.",
1265
+ "since": "0.6.0",
1266
+ "tags": [
1267
+ "trigger"
1268
+ ],
1269
+ "input": {
1270
+ "schema": {
1271
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
1272
+ "version": 1,
1273
+ "graph": {
1274
+ "nodes": [
1275
+ {
1276
+ "id": "s",
1277
+ "kind": "schedule_trigger",
1278
+ "position": {
1279
+ "x": 0,
1280
+ "y": 0
1281
+ },
1282
+ "config": {
1283
+ "cron": "*/5 * * * *",
1284
+ "timezone": "UTC"
1285
+ }
1286
+ },
1287
+ {
1288
+ "id": "o",
1289
+ "kind": "output",
1290
+ "position": {
1291
+ "x": 1,
1292
+ "y": 0
1293
+ }
1294
+ }
1295
+ ],
1296
+ "edges": [
1297
+ {
1298
+ "id": "e1",
1299
+ "source": "s",
1300
+ "target": "o"
1301
+ }
1302
+ ]
1303
+ }
1304
+ },
1305
+ "initialInputs": {
1306
+ "s": {
1307
+ "tick": 1
1308
+ }
1309
+ }
1310
+ },
1311
+ "expected": {
1312
+ "ok": true,
1313
+ "outputs": {
1314
+ "s": {
1315
+ "cron": "*/5 * * * *",
1316
+ "timezone": "UTC",
1317
+ "tick": 1
1318
+ },
1319
+ "o": {
1320
+ "cron": "*/5 * * * *",
1321
+ "timezone": "UTC",
1322
+ "tick": 1
1323
+ }
1324
+ }
1325
+ }
1326
+ },
1327
+ {
1328
+ "id": "0017-user-input",
1329
+ "title": "User input surfaces submitted values.",
1330
+ "since": "0.6.0",
1331
+ "tags": [
1332
+ "human"
1333
+ ],
1334
+ "input": {
1335
+ "schema": {
1336
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
1337
+ "version": 1,
1338
+ "graph": {
1339
+ "nodes": [
1340
+ {
1341
+ "id": "u",
1342
+ "kind": "user_input",
1343
+ "position": {
1344
+ "x": 0,
1345
+ "y": 0
1346
+ },
1347
+ "config": {
1348
+ "title": "Confirm"
1349
+ }
1350
+ },
1351
+ {
1352
+ "id": "o",
1353
+ "kind": "output",
1354
+ "position": {
1355
+ "x": 1,
1356
+ "y": 0
1357
+ }
1358
+ }
1359
+ ],
1360
+ "edges": [
1361
+ {
1362
+ "id": "e1",
1363
+ "source": "u",
1364
+ "target": "o"
1365
+ }
1366
+ ]
1367
+ }
1368
+ },
1369
+ "initialInputs": {
1370
+ "u": {
1371
+ "values": {
1372
+ "answer": "yes"
1373
+ }
1374
+ }
1375
+ }
1376
+ },
1377
+ "expected": {
1378
+ "ok": true,
1379
+ "outputs": {
1380
+ "u": {
1381
+ "answer": "yes"
1382
+ },
1383
+ "o": {
1384
+ "answer": "yes"
1385
+ }
1386
+ }
1387
+ }
1388
+ },
1389
+ {
1390
+ "id": "0018-human-approval",
1391
+ "title": "Approval routes to the approved branch.",
1392
+ "since": "0.6.0",
1393
+ "tags": [
1394
+ "human",
1395
+ "branch"
1396
+ ],
1397
+ "input": {
1398
+ "schema": {
1399
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
1400
+ "version": 1,
1401
+ "graph": {
1402
+ "nodes": [
1403
+ {
1404
+ "id": "t",
1405
+ "kind": "manual_trigger",
1406
+ "position": {
1407
+ "x": 0,
1408
+ "y": 0
1409
+ }
1410
+ },
1411
+ {
1412
+ "id": "ap",
1413
+ "kind": "human_approval",
1414
+ "position": {
1415
+ "x": 1,
1416
+ "y": 0
1417
+ },
1418
+ "config": {
1419
+ "title": "Ship it?"
1420
+ }
1421
+ },
1422
+ {
1423
+ "id": "o",
1424
+ "kind": "output",
1425
+ "position": {
1426
+ "x": 2,
1427
+ "y": 0
1428
+ }
1429
+ }
1430
+ ],
1431
+ "edges": [
1432
+ {
1433
+ "id": "e1",
1434
+ "source": "t",
1435
+ "target": "ap"
1436
+ },
1437
+ {
1438
+ "id": "e2",
1439
+ "source": "ap",
1440
+ "target": "o",
1441
+ "sourceHandle": "approved"
1442
+ }
1443
+ ]
1444
+ }
1445
+ },
1446
+ "initialInputs": {
1447
+ "t": {
1448
+ "approved": true,
1449
+ "in": {
1450
+ "release": "1.0"
1451
+ }
1452
+ }
1453
+ }
1454
+ },
1455
+ "expected": {
1456
+ "ok": true,
1457
+ "outputs": {
1458
+ "t": {
1459
+ "approved": true,
1460
+ "in": {
1461
+ "release": "1.0"
1462
+ }
1463
+ },
1464
+ "ap": {
1465
+ "branch": "approved",
1466
+ "value": {
1467
+ "approved": true,
1468
+ "in": {
1469
+ "release": "1.0"
1470
+ }
1471
+ }
1472
+ },
1473
+ "o": {
1474
+ "approved": true,
1475
+ "in": {
1476
+ "release": "1.0"
1477
+ }
1478
+ }
1479
+ }
1480
+ }
1481
+ },
1482
+ {
1483
+ "id": "0019-wait",
1484
+ "title": "Wait passes through without sleeping (framework-free).",
1485
+ "since": "0.6.0",
1486
+ "tags": [
1487
+ "logic"
1488
+ ],
1489
+ "input": {
1490
+ "schema": {
1491
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
1492
+ "version": 1,
1493
+ "graph": {
1494
+ "nodes": [
1495
+ {
1496
+ "id": "t",
1497
+ "kind": "manual_trigger",
1498
+ "position": {
1499
+ "x": 0,
1500
+ "y": 0
1501
+ }
1502
+ },
1503
+ {
1504
+ "id": "w",
1505
+ "kind": "wait",
1506
+ "position": {
1507
+ "x": 1,
1508
+ "y": 0
1509
+ },
1510
+ "config": {
1511
+ "mode": "duration",
1512
+ "duration": "10s"
1513
+ }
1514
+ }
1515
+ ],
1516
+ "edges": [
1517
+ {
1518
+ "id": "e1",
1519
+ "source": "t",
1520
+ "target": "w"
1521
+ }
1522
+ ]
1523
+ }
1524
+ },
1525
+ "initialInputs": {
1526
+ "t": {
1527
+ "job": 7
1528
+ }
1529
+ }
1530
+ },
1531
+ "expected": {
1532
+ "ok": true,
1533
+ "outputs": {
1534
+ "t": {
1535
+ "job": 7
1536
+ },
1537
+ "w": {
1538
+ "waited": "duration",
1539
+ "duration": "10s",
1540
+ "input": {
1541
+ "job": 7
1542
+ }
1543
+ }
1544
+ }
1545
+ }
1546
+ },
1547
+ {
1548
+ "id": "0020-subgraph",
1549
+ "title": "A subgraph runs a nested transform → output.",
1550
+ "since": "0.6.0",
1551
+ "tags": [
1552
+ "structural"
1553
+ ],
1554
+ "input": {
1555
+ "schema": {
1556
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
1557
+ "version": 1,
1558
+ "graph": {
1559
+ "nodes": [
1560
+ {
1561
+ "id": "t",
1562
+ "kind": "manual_trigger",
1563
+ "position": {
1564
+ "x": 0,
1565
+ "y": 0
1566
+ }
1567
+ },
1568
+ {
1569
+ "id": "sg",
1570
+ "kind": "subgraph",
1571
+ "position": {
1572
+ "x": 1,
1573
+ "y": 0
1574
+ },
1575
+ "config": {
1576
+ "graph": {
1577
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
1578
+ "version": 1,
1579
+ "graph": {
1580
+ "nodes": [
1581
+ {
1582
+ "id": "itf",
1583
+ "kind": "transform",
1584
+ "position": {
1585
+ "x": 0,
1586
+ "y": 0
1587
+ },
1588
+ "config": {
1589
+ "expression": "{{ $json.n }}"
1590
+ }
1591
+ },
1592
+ {
1593
+ "id": "io",
1594
+ "kind": "output",
1595
+ "position": {
1596
+ "x": 1,
1597
+ "y": 0
1598
+ }
1599
+ }
1600
+ ],
1601
+ "edges": [
1602
+ {
1603
+ "id": "ie",
1604
+ "source": "itf",
1605
+ "target": "io"
1606
+ }
1607
+ ]
1608
+ }
1609
+ }
1610
+ }
1611
+ }
1612
+ ],
1613
+ "edges": [
1614
+ {
1615
+ "id": "e1",
1616
+ "source": "t",
1617
+ "target": "sg"
1618
+ }
1619
+ ]
1620
+ }
1621
+ },
1622
+ "initialInputs": {
1623
+ "t": {
1624
+ "n": 99
1625
+ }
1626
+ }
1627
+ },
1628
+ "expected": {
1629
+ "ok": true,
1630
+ "outputs": {
1631
+ "t": {
1632
+ "n": 99
1633
+ },
1634
+ "sg": {
1635
+ "itf": 99,
1636
+ "io": 99
1637
+ }
1638
+ }
1639
+ }
1640
+ },
1641
+ {
1642
+ "id": "0021-cycle",
1643
+ "title": "A cyclic graph aborts.",
1644
+ "since": "0.6.0",
1645
+ "tags": [
1646
+ "error",
1647
+ "topology"
1648
+ ],
1649
+ "input": {
1650
+ "schema": {
1651
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
1652
+ "version": 1,
1653
+ "graph": {
1654
+ "nodes": [
1655
+ {
1656
+ "id": "a",
1657
+ "kind": "transform",
1658
+ "position": {
1659
+ "x": 0,
1660
+ "y": 0
1661
+ },
1662
+ "config": {
1663
+ "expression": "x"
1664
+ }
1665
+ },
1666
+ {
1667
+ "id": "b",
1668
+ "kind": "transform",
1669
+ "position": {
1670
+ "x": 1,
1671
+ "y": 0
1672
+ },
1673
+ "config": {
1674
+ "expression": "y"
1675
+ }
1676
+ }
1677
+ ],
1678
+ "edges": [
1679
+ {
1680
+ "id": "e1",
1681
+ "source": "a",
1682
+ "target": "b"
1683
+ },
1684
+ {
1685
+ "id": "e2",
1686
+ "source": "b",
1687
+ "target": "a"
1688
+ }
1689
+ ]
1690
+ }
1691
+ },
1692
+ "initialInputs": {}
1693
+ },
1694
+ "expected": {
1695
+ "ok": false,
1696
+ "error": "Cycle detected in flow graph — aborting."
1697
+ },
1698
+ "notes": "The exact message matters and is why this case is not `errorContains`. PHP and TypeScript emit an EM DASH; the Python port emitted an ASCII hyphen and nothing reported it for two releases, because the substring assertion this replaces stopped before the character they disagreed on."
1699
+ },
1700
+ {
1701
+ "id": "0022-unknown-kind",
1702
+ "title": "An unregistered kind has no executor and errors at run time.",
1703
+ "since": "0.6.0",
1704
+ "tags": [
1705
+ "error",
1706
+ "executors"
1707
+ ],
1708
+ "input": {
1709
+ "schema": {
1710
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
1711
+ "version": 1,
1712
+ "graph": {
1713
+ "nodes": [
1714
+ {
1715
+ "id": "x",
1716
+ "kind": "no_such_kind",
1717
+ "position": {
1718
+ "x": 0,
1719
+ "y": 0
1720
+ }
1721
+ }
1722
+ ],
1723
+ "edges": []
1724
+ }
1725
+ },
1726
+ "initialInputs": {}
1727
+ },
1728
+ "expected": {
1729
+ "ok": false,
1730
+ "error": "No executor registered for kind=no_such_kind"
1731
+ },
1732
+ "notes": "An unregistered kind fails CLOSED — no executor, no outputs, and the run stops. That silence is the right default and is exactly what made a kind-keyed registry lookup miss go unnoticed in the TypeScript engine until 0.48.1."
1733
+ },
1734
+ {
1735
+ "id": "0023-merge-same-handle",
1736
+ "title": "Two mutually-exclusive branches rejoin on the SAME target handle. 05 covers the merge-node shape, where each branch lands on its own handle and so cannot collide; this covers the shape that can: the dead branch must not clobber the live one's value with nothing. Ordering the dead edge LAST is what makes it bite.",
1737
+ "since": "0.6.0",
1738
+ "tags": [
1739
+ "branch",
1740
+ "merge",
1741
+ "hazard"
1742
+ ],
1743
+ "input": {
1744
+ "schema": {
1745
+ "$schema": "https://particle.academy/schemas/workflow/v1.json",
1746
+ "version": 1,
1747
+ "graph": {
1748
+ "nodes": [
1749
+ {
1750
+ "id": "t",
1751
+ "kind": "manual_trigger",
1752
+ "position": {
1753
+ "x": 0,
1754
+ "y": 0
1755
+ }
1756
+ },
1757
+ {
1758
+ "id": "br",
1759
+ "kind": "branch",
1760
+ "position": {
1761
+ "x": 1,
1762
+ "y": 0
1763
+ },
1764
+ "config": {
1765
+ "condition": "{{ $json.ok }}"
1766
+ }
1767
+ },
1768
+ {
1769
+ "id": "a",
1770
+ "kind": "transform",
1771
+ "position": {
1772
+ "x": 2,
1773
+ "y": 0
1774
+ },
1775
+ "config": {
1776
+ "expression": "A"
1777
+ }
1778
+ },
1779
+ {
1780
+ "id": "b",
1781
+ "kind": "transform",
1782
+ "position": {
1783
+ "x": 2,
1784
+ "y": 1
1785
+ },
1786
+ "config": {
1787
+ "expression": "B"
1788
+ }
1789
+ },
1790
+ {
1791
+ "id": "o",
1792
+ "kind": "output",
1793
+ "position": {
1794
+ "x": 3,
1795
+ "y": 0
1796
+ }
1797
+ }
1798
+ ],
1799
+ "edges": [
1800
+ {
1801
+ "id": "e1",
1802
+ "source": "t",
1803
+ "target": "br"
1804
+ },
1805
+ {
1806
+ "id": "e2",
1807
+ "source": "br",
1808
+ "target": "a",
1809
+ "sourceHandle": "true"
1810
+ },
1811
+ {
1812
+ "id": "e3",
1813
+ "source": "br",
1814
+ "target": "b",
1815
+ "sourceHandle": "false"
1816
+ },
1817
+ {
1818
+ "id": "e4",
1819
+ "source": "a",
1820
+ "target": "o"
1821
+ },
1822
+ {
1823
+ "id": "e5",
1824
+ "source": "b",
1825
+ "target": "o"
1826
+ }
1827
+ ]
1828
+ }
1829
+ },
1830
+ "initialInputs": {
1831
+ "t": {
1832
+ "ok": true
1833
+ }
1834
+ }
1835
+ },
1836
+ "expected": {
1837
+ "ok": true,
1838
+ "outputs": {
1839
+ "t": {
1840
+ "ok": true
1841
+ },
1842
+ "br": {
1843
+ "branch": "true",
1844
+ "value": {
1845
+ "ok": true
1846
+ }
1847
+ },
1848
+ "a": "A",
1849
+ "o": "A"
1850
+ }
1851
+ },
1852
+ "notes": "Two mutually-exclusive branches rejoin on the SAME target handle. `collectInputs` must read only ACTIVE edges: assigning unconditionally lets the dead branch's absent value clobber the live one whenever it happens to come last, emptying the merge point while the run still reports success. TypeScript implemented the code and PHP the documented contract; they disagreed silently until fancy-flow 0.27.1."
1853
+ }
1854
+ ]
1855
+ }