Skip to main content

nika_init/
showcase_patterns.rs

1//! Showcase Patterns — 15 workflows exercising for_each + structured output
2//!
3//! These workflows demonstrate data processing patterns that combine
4//! parallel iteration (`for_each:`) with schema-validated output
5//! (`structured:`). They cover the hardest features to get right.
6//!
7//! All 15 workflows pass `nika check`. LLM workflows use `{{PROVIDER}}`
8//! and `{{MODEL}}` placeholders replaced at generation time.
9
10use super::WorkflowTemplate;
11
12// ═════════════════════════════════════════════════════════════════════════════
13// 01: Multi-URL Status Checker
14// ═════════════════════════════════════════════════════════════════════════════
15
16pub const SHOWCASE_01_URL_STATUS: &str = r##"# ═══════════════════════════════════════════════════════════════════
17# Pattern 01: Multi-URL Status Checker
18# ═══════════════════════════════════════════════════════════════════
19#
20# for_each over 10 URLs, fetch each, produce structured status report.
21# Demonstrates: for_each array literal + fetch + structured output.
22#
23# Prerequisites: None (public endpoints)
24# Run: nika run workflows/showcase/01-url-status.nika.yaml
25
26schema: "nika/workflow@0.12"
27workflow: url-status-checker
28description: "Check status of multiple URLs in parallel"
29
30tasks:
31  - id: check_urls
32    for_each:
33      - "https://httpbin.org/status/200"
34      - "https://httpbin.org/status/201"
35      - "https://httpbin.org/status/301"
36      - "https://httpbin.org/status/404"
37      - "https://httpbin.org/status/500"
38      - "https://httpbin.org/ip"
39      - "https://httpbin.org/uuid"
40      - "https://httpbin.org/user-agent"
41      - "https://httpbin.org/headers"
42      - "https://httpbin.org/get"
43    as: target_url
44    concurrency: 5
45    fail_fast: false
46    fetch:
47      url: "{{with.target_url}}"
48      method: GET
49      response: full
50      timeout: 15
51
52  - id: report
53    depends_on: [check_urls]
54    with:
55      results: $check_urls
56    exec:
57      command: |
58        echo 'Status check complete. {{with.results | length}} URLs checked.'
59      shell: true
60"##;
61
62// ═════════════════════════════════════════════════════════════════════════════
63// 02: Language Detector
64// ═════════════════════════════════════════════════════════════════════════════
65
66pub const SHOWCASE_02_LANG_DETECT: &str = r##"# ═══════════════════════════════════════════════════════════════════
67# Pattern 02: Language Detector
68# ═══════════════════════════════════════════════════════════════════
69#
70# for_each over 5 multilingual texts, detect language via LLM,
71# return structured {text_snippet, language, confidence}.
72#
73# Prerequisites: LLM provider
74# Run: nika run workflows/showcase/02-lang-detect.nika.yaml
75
76schema: "nika/workflow@0.12"
77workflow: language-detector
78description: "Detect language of multiple texts with structured output"
79
80provider: "{{PROVIDER}}"
81model: "{{MODEL}}"
82
83tasks:
84  - id: detect
85    for_each:
86      - "Bonjour, comment allez-vous aujourd'hui?"
87      - "The quick brown fox jumps over the lazy dog."
88      - "Heute ist ein wunderschoener Tag zum Programmieren."
89      - "El desarrollo de software es un arte y una ciencia."
90      - "Konnichiwa, kyou wa ii tenki desu ne."
91    as: text
92    concurrency: 3
93    structured:
94      schema:
95        type: object
96        properties:
97          text_snippet:
98            type: string
99            description: "First 40 characters of the input"
100          language:
101            type: string
102            enum: ["english", "french", "german", "spanish", "japanese", "other"]
103          confidence:
104            type: number
105            minimum: 0
106            maximum: 1
107        required: [text_snippet, language, confidence]
108    infer:
109      prompt: |
110        Detect the language of this text. Return the first 40 characters
111        as text_snippet, the detected language, and a confidence score 0-1.
112
113        Text: "{{with.text}}"
114
115  - id: summary
116    depends_on: [detect]
117    with:
118      detections: $detect
119    exec:
120      command: |
121        echo 'Language detection complete. {{with.detections | length}} texts processed.'
122      shell: true
123"##;
124
125// ═════════════════════════════════════════════════════════════════════════════
126// 03: Batch Sentiment Analysis
127// ═════════════════════════════════════════════════════════════════════════════
128
129pub const SHOWCASE_03_SENTIMENT: &str = r##"# ═══════════════════════════════════════════════════════════════════
130# Pattern 03: Batch Sentiment Analysis
131# ═══════════════════════════════════════════════════════════════════
132#
133# for_each over product reviews, analyze sentiment with structured output.
134# Demonstrates: for_each + structured with enum + number constraints.
135#
136# Prerequisites: LLM provider
137# Run: nika run workflows/showcase/03-sentiment.nika.yaml
138
139schema: "nika/workflow@0.12"
140workflow: batch-sentiment
141description: "Analyze sentiment of product reviews in batch"
142
143provider: "{{PROVIDER}}"
144model: "{{MODEL}}"
145
146tasks:
147  - id: analyze
148    for_each:
149      - "Absolutely love this product! Best purchase I've made all year."
150      - "Decent quality but the shipping took forever. Not great, not terrible."
151      - "Complete waste of money. Broke after two days. Would NOT recommend."
152      - "It works as advertised. Nothing special but gets the job done."
153      - "Mind-blowing performance! This exceeded all my expectations."
154      - "Meh. It's okay I guess. Expected more for the price."
155    as: review
156    concurrency: 3
157    structured:
158      schema:
159        type: object
160        properties:
161          review_excerpt:
162            type: string
163            maxLength: 60
164            description: "First 60 chars of the review"
165          sentiment:
166            type: string
167            enum: ["positive", "neutral", "negative"]
168          score:
169            type: number
170            minimum: -1
171            maximum: 1
172            description: "Sentiment score from -1 (negative) to 1 (positive)"
173          key_phrases:
174            type: array
175            items:
176              type: string
177            maxItems: 3
178            description: "Key sentiment-bearing phrases"
179        required: [review_excerpt, sentiment, score, key_phrases]
180    infer:
181      prompt: |
182        Analyze the sentiment of this product review.
183        Return: excerpt (first 60 chars), sentiment label,
184        score (-1 to 1), and up to 3 key phrases.
185
186        Review: "{{with.review}}"
187      temperature: 0.2
188
189  - id: aggregate
190    depends_on: [analyze]
191    with:
192      results: $analyze
193    exec:
194      command: |
195        echo 'Sentiment analysis complete. {{with.results | length}} reviews analyzed.'
196      shell: true
197"##;
198
199// ═════════════════════════════════════════════════════════════════════════════
200// 04: Parallel Translation
201// ═════════════════════════════════════════════════════════════════════════════
202
203pub const SHOWCASE_04_TRANSLATION: &str = r##"# ═══════════════════════════════════════════════════════════════════
204# Pattern 04: Parallel Translation
205# ═══════════════════════════════════════════════════════════════════
206#
207# Translate a source text into 5 languages in parallel.
208# Each translation is saved as a separate artifact.
209#
210# Prerequisites: LLM provider
211# Run: nika run workflows/showcase/04-translation.nika.yaml
212
213schema: "nika/workflow@0.12"
214workflow: parallel-translation
215description: "Translate text into 5 languages with per-language artifacts"
216
217provider: "{{PROVIDER}}"
218model: "{{MODEL}}"
219
220tasks:
221  - id: source_text
222    exec:
223      command: |
224        echo 'Open source software is not just about code. It is about community, collaboration, and the belief that knowledge should be free. Every contribution, no matter how small, moves us forward together.'
225      shell: true
226
227  - id: translate
228    depends_on: [source_text]
229    with:
230      source: $source_text
231    for_each: ["french", "german", "spanish", "japanese", "portuguese"]
232    as: lang
233    concurrency: 5
234    structured:
235      schema:
236        type: object
237        properties:
238          language:
239            type: string
240          translation:
241            type: string
242            minLength: 10
243          word_count:
244            type: integer
245            minimum: 1
246        required: [language, translation, word_count]
247    infer:
248      prompt: |
249        Translate the following text into {{with.lang}}.
250        Return the target language name, the translation, and the word count.
251
252        Source text: "{{with.source}}"
253      temperature: 0.3
254    artifact:
255      path: "output/translations/{{with.lang}}.json"
256      format: json
257
258  - id: done
259    depends_on: [translate]
260    exec:
261      command: "echo 'All 5 translations complete.'"
262      shell: true
263"##;
264
265// ═════════════════════════════════════════════════════════════════════════════
266// 05: API Endpoint Tester
267// ═════════════════════════════════════════════════════════════════════════════
268
269pub const SHOWCASE_05_API_TESTER: &str = r##"# ═══════════════════════════════════════════════════════════════════
270# Pattern 05: API Endpoint Tester
271# ═══════════════════════════════════════════════════════════════════
272#
273# Test multiple API endpoints with full response inspection.
274# Demonstrates: for_each + fetch response:full + structured report.
275#
276# Prerequisites: None (public endpoints)
277# Run: nika run workflows/showcase/05-api-tester.nika.yaml
278
279schema: "nika/workflow@0.12"
280workflow: api-endpoint-tester
281description: "Test API endpoints and report structured results"
282
283provider: "{{PROVIDER}}"
284model: "{{MODEL}}"
285
286tasks:
287  - id: test_endpoints
288    for_each:
289      - "https://httpbin.org/get"
290      - "https://httpbin.org/ip"
291      - "https://httpbin.org/uuid"
292      - "https://httpbin.org/user-agent"
293      - "https://httpbin.org/headers"
294      - "https://httpbin.org/delay/1"
295    as: endpoint
296    concurrency: 3
297    fail_fast: false
298    fetch:
299      url: "{{with.endpoint}}"
300      method: GET
301      response: full
302      timeout: 10
303
304  - id: analyze_results
305    depends_on: [test_endpoints]
306    with:
307      raw_results: $test_endpoints
308    structured:
309      schema:
310        type: object
311        properties:
312          total_endpoints:
313            type: integer
314          healthy_count:
315            type: integer
316          slow_count:
317            type: integer
318            description: "Endpoints with response time > 2 seconds"
319          summary:
320            type: string
321            description: "Brief health summary"
322        required: [total_endpoints, healthy_count, slow_count, summary]
323    infer:
324      prompt: |
325        Analyze these API test results and produce a health report.
326        Count total endpoints, healthy ones (2xx), and slow ones (>2s).
327
328        Results: {{with.raw_results}}
329      temperature: 0.1
330"##;
331
332// ═════════════════════════════════════════════════════════════════════════════
333// 06: Markdown Link Checker
334// ═════════════════════════════════════════════════════════════════════════════
335
336pub const SHOWCASE_06_LINK_CHECKER: &str = r##"# ═══════════════════════════════════════════════════════════════════
337# Pattern 06: Markdown Link Checker
338# ═══════════════════════════════════════════════════════════════════
339#
340# Extract links via exec, then for_each to verify each link.
341# Demonstrates: exec output → for_each binding → fetch → structured.
342#
343# Prerequisites: None
344# Run: nika run workflows/showcase/06-link-checker.nika.yaml
345
346schema: "nika/workflow@0.12"
347workflow: link-checker
348description: "Extract and verify links from a list"
349
350tasks:
351  - id: extract_links
352    exec:
353      command: |
354        echo '["https://httpbin.org/status/200", "https://httpbin.org/status/404", "https://httpbin.org/status/301", "https://httpbin.org/get", "https://httpbin.org/status/500"]'
355      shell: true
356
357  - id: check_each
358    depends_on: [extract_links]
359    with:
360      links: $extract_links | parse_json
361    for_each: "{{with.links}}"
362    as: url
363    concurrency: 3
364    fail_fast: false
365    fetch:
366      url: "{{with.url}}"
367      method: GET
368      response: full
369      timeout: 10
370
371  - id: report
372    depends_on: [check_each]
373    with:
374      results: $check_each
375    exec:
376      command: |
377        echo 'Link check complete. Results: {{with.results}}'
378      shell: true
379"##;
380
381// ═════════════════════════════════════════════════════════════════════════════
382// 07: RSS Multi-Feed Aggregator
383// ═════════════════════════════════════════════════════════════════════════════
384
385pub const SHOWCASE_07_RSS_AGGREGATOR: &str = r##"# ═══════════════════════════════════════════════════════════════════
386# Pattern 07: RSS Multi-Feed Aggregator
387# ═══════════════════════════════════════════════════════════════════
388#
389# Fetch 5 RSS feeds in parallel with extract:feed, then merge.
390# Demonstrates: for_each + fetch extract:feed + aggregation.
391#
392# Prerequisites: None (public feeds)
393# Run: nika run workflows/showcase/07-rss-aggregator.nika.yaml
394
395schema: "nika/workflow@0.12"
396workflow: rss-aggregator
397description: "Aggregate multiple RSS feeds in parallel"
398
399tasks:
400  - id: fetch_feeds
401    for_each:
402      - "https://hnrss.org/newest?count=3"
403      - "https://www.reddit.com/r/rust/.rss?limit=3"
404      - "https://www.reddit.com/r/programming/.rss?limit=3"
405      - "https://lobste.rs/rss"
406      - "https://blog.rust-lang.org/feed.xml"
407    as: feed_url
408    concurrency: 5
409    fail_fast: false
410    fetch:
411      url: "{{with.feed_url}}"
412      extract: feed
413      timeout: 15
414
415  - id: merge
416    depends_on: [fetch_feeds]
417    with:
418      feeds: $fetch_feeds
419    exec:
420      command: |
421        echo 'Aggregated {{with.feeds | length}} feeds.'
422      shell: true
423    artifact:
424      path: output/feeds/aggregated.json
425      format: json
426"##;
427
428// ═════════════════════════════════════════════════════════════════════════════
429// 08: Git Branch Analyzer
430// ═════════════════════════════════════════════════════════════════════════════
431
432pub const SHOWCASE_08_GIT_ANALYZER: &str = r##"# ═══════════════════════════════════════════════════════════════════
433# Pattern 08: Git Branch Analyzer
434# ═══════════════════════════════════════════════════════════════════
435#
436# List git branches, then for_each branch get last commit info.
437# Demonstrates: exec → for_each binding → exec per item → structured.
438#
439# Prerequisites: Must run inside a git repository, LLM provider
440# Run: nika run workflows/showcase/08-git-analyzer.nika.yaml
441
442schema: "nika/workflow@0.12"
443workflow: git-branch-analyzer
444description: "Analyze git branches with structured summary"
445
446provider: "{{PROVIDER}}"
447model: "{{MODEL}}"
448
449tasks:
450  - id: list_branches
451    exec:
452      command: |
453        git branch --format='%(refname:short)' | head -5 | tr '\n' ',' | sed 's/,$//' | xargs -I{} echo '["{}"' | sed 's/,/","/g' | sed 's/\["/["/' | sed 's/$/"]/'
454      shell: true
455
456  - id: get_branch_info
457    depends_on: [list_branches]
458    with:
459      branches: $list_branches | parse_json
460    for_each: "{{with.branches}}"
461    as: branch
462    concurrency: 3
463    exec:
464      command: |
465        echo "Branch: {{with.branch}} | Last commit: $(git log -1 --format='%s (%ar)' {{with.branch}} 2>/dev/null || echo 'unknown')"
466      shell: true
467
468  - id: summarize
469    depends_on: [get_branch_info]
470    with:
471      branch_data: $get_branch_info
472    structured:
473      schema:
474        type: object
475        properties:
476          total_branches:
477            type: integer
478          branch_summaries:
479            type: array
480            items:
481              type: object
482              properties:
483                name:
484                  type: string
485                last_activity:
486                  type: string
487              required: [name, last_activity]
488          recommendation:
489            type: string
490            description: "Suggestion for branch cleanup"
491        required: [total_branches, branch_summaries, recommendation]
492    infer:
493      prompt: |
494        Analyze this git branch information and produce a summary.
495        Include total branches, a summary for each, and cleanup recommendations.
496
497        Branch data: {{with.branch_data}}
498      temperature: 0.2
499"##;
500
501// ═════════════════════════════════════════════════════════════════════════════
502// 09: Package Version Checker
503// ═════════════════════════════════════════════════════════════════════════════
504
505pub const SHOWCASE_09_PKG_VERSIONS: &str = r##"# ═══════════════════════════════════════════════════════════════════
506# Pattern 09: Package Version Checker
507# ═══════════════════════════════════════════════════════════════════
508#
509# Check latest versions of npm/crates packages via API.
510# Demonstrates: for_each + fetch JSON API + extract:jsonpath + structured.
511#
512# Prerequisites: LLM provider (for summary)
513# Run: nika run workflows/showcase/09-pkg-versions.nika.yaml
514
515schema: "nika/workflow@0.12"
516workflow: package-version-checker
517description: "Check latest package versions from crates.io"
518
519provider: "{{PROVIDER}}"
520model: "{{MODEL}}"
521
522tasks:
523  - id: check_crates
524    for_each: ["serde", "tokio", "anyhow", "clap", "tracing"]
525    as: crate_name
526    concurrency: 3
527    retry:
528      max_attempts: 3
529      delay_ms: 1000
530      backoff: 2.0
531    fetch:
532      url: "https://crates.io/api/v1/crates/{{with.crate_name}}"
533      method: GET
534      headers:
535        User-Agent: "nika-workflow/0.1"
536      extract: jsonpath
537      selector: "$.crate.newest_version"
538      timeout: 10
539
540  - id: version_report
541    depends_on: [check_crates]
542    with:
543      versions: $check_crates
544    structured:
545      schema:
546        type: object
547        properties:
548          packages:
549            type: array
550            items:
551              type: object
552              properties:
553                name:
554                  type: string
555                latest_version:
556                  type: string
557                status:
558                  type: string
559                  enum: ["up-to-date", "outdated", "unknown"]
560              required: [name, latest_version, status]
561          summary:
562            type: string
563        required: [packages, summary]
564    infer:
565      prompt: |
566        Based on these crate version check results, produce a dependency report.
567        List each package with its latest version and status.
568
569        Version data: {{with.versions}}
570      temperature: 0.1
571"##;
572
573// ═════════════════════════════════════════════════════════════════════════════
574// 10: Concurrent Web Scraper
575// ═════════════════════════════════════════════════════════════════════════════
576
577pub const SHOWCASE_10_WEB_SCRAPER: &str = r##"# ═══════════════════════════════════════════════════════════════════
578# Pattern 10: Concurrent Web Scraper
579# ═══════════════════════════════════════════════════════════════════
580#
581# Scrape multiple URLs with rate-limited concurrency (max 3).
582# Each page extracted as markdown and saved as artifact.
583#
584# Prerequisites: None
585# Run: nika run workflows/showcase/10-web-scraper.nika.yaml
586
587schema: "nika/workflow@0.12"
588workflow: concurrent-scraper
589description: "Scrape multiple pages with rate-limited concurrency"
590
591tasks:
592  - id: scrape
593    for_each:
594      - "https://httpbin.org/html"
595      - "https://httpbin.org/robots.txt"
596      - "https://httpbin.org/forms/post"
597      - "https://httpbin.org/links/5"
598      - "https://httpbin.org/xml"
599    as: page_url
600    concurrency: 3
601    fail_fast: false
602    fetch:
603      url: "{{with.page_url}}"
604      extract: markdown
605      timeout: 15
606
607  - id: done
608    depends_on: [scrape]
609    with:
610      pages: $scrape
611    exec:
612      command: |
613        echo 'Scraping complete. {{with.pages | length}} pages collected.'
614      shell: true
615"##;
616
617// ═════════════════════════════════════════════════════════════════════════════
618// 11: Multi-Format Export
619// ═════════════════════════════════════════════════════════════════════════════
620
621pub const SHOWCASE_11_MULTI_FORMAT: &str = r##"# ═══════════════════════════════════════════════════════════════════
622# Pattern 11: Multi-Format Export
623# ═══════════════════════════════════════════════════════════════════
624#
625# Generate content once, then export to 4 formats via for_each.
626# Each format produces a separate artifact.
627#
628# Prerequisites: LLM provider
629# Run: nika run workflows/showcase/11-multi-format.nika.yaml
630
631schema: "nika/workflow@0.12"
632workflow: multi-format-export
633description: "Generate content and export to multiple formats"
634
635provider: "{{PROVIDER}}"
636model: "{{MODEL}}"
637
638tasks:
639  - id: generate
640    structured:
641      schema:
642        type: object
643        properties:
644          title:
645            type: string
646          summary:
647            type: string
648            maxLength: 200
649          sections:
650            type: array
651            items:
652              type: object
653              properties:
654                heading:
655                  type: string
656                body:
657                  type: string
658              required: [heading, body]
659            minItems: 2
660            maxItems: 4
661        required: [title, summary, sections]
662    infer:
663      prompt: |
664        Create a short technical document about "DAG-based workflow engines".
665        Include a title, summary (max 200 chars), and 2-4 sections.
666      temperature: 0.5
667      max_tokens: 800
668
669  - id: export
670    depends_on: [generate]
671    with:
672      content: $generate
673    for_each: ["text", "json", "yaml", "html"]
674    as: fmt
675    concurrency: 4
676    exec:
677      command: |
678        echo 'Exporting as {{with.fmt}}: {{with.content}}'
679      shell: true
680    artifact:
681      path: "output/exports/document.{{with.fmt}}"
682
683  - id: complete
684    depends_on: [export]
685    exec:
686      command: "echo 'All 4 format exports complete.'"
687      shell: true
688"##;
689
690// ═════════════════════════════════════════════════════════════════════════════
691// 12: Batch Image Analysis
692// ═════════════════════════════════════════════════════════════════════════════
693
694pub const SHOWCASE_12_IMAGE_ANALYSIS: &str = r##"# ═══════════════════════════════════════════════════════════════════
695# Pattern 12: Batch Image Analysis
696# ═══════════════════════════════════════════════════════════════════
697#
698# Download images, then for_each run nika:dimensions + nika:thumbhash.
699# Demonstrates: for_each + fetch binary + invoke builtins + structured.
700#
701# Prerequisites: LLM provider (for summary)
702# Run: nika run workflows/showcase/12-image-analysis.nika.yaml
703
704schema: "nika/workflow@0.12"
705workflow: batch-image-analysis
706description: "Analyze multiple images with builtin tools"
707
708provider: "{{PROVIDER}}"
709model: "{{MODEL}}"
710
711tasks:
712  - id: download_images
713    for_each:
714      - "https://httpbin.org/image/png"
715      - "https://httpbin.org/image/jpeg"
716      - "https://httpbin.org/image/svg"
717    as: img_url
718    concurrency: 3
719    fetch:
720      url: "{{with.img_url}}"
721      response: binary
722      timeout: 15
723
724  - id: get_dimensions
725    depends_on: [download_images]
726    with:
727      images: $download_images
728    for_each: "{{with.images}}"
729    as: img_hash
730    concurrency: 3
731    invoke:
732      tool: "nika:dimensions"
733      params:
734        hash: "{{with.img_hash}}"
735
736  - id: summarize
737    depends_on: [get_dimensions]
738    with:
739      dimension_data: $get_dimensions
740    structured:
741      schema:
742        type: object
743        properties:
744          images_analyzed:
745            type: integer
746          results:
747            type: array
748            items:
749              type: object
750              properties:
751                format:
752                  type: string
753                width:
754                  type: integer
755                height:
756                  type: integer
757              required: [format, width, height]
758          total_pixels:
759            type: integer
760        required: [images_analyzed, results, total_pixels]
761    infer:
762      prompt: |
763        Summarize the image dimension data into a structured report.
764        Include per-image format/width/height and total pixel count.
765
766        Dimension data: {{with.dimension_data}}
767      temperature: 0.1
768"##;
769
770// ═════════════════════════════════════════════════════════════════════════════
771// 13: Nested For Each (categories × items)
772// ═════════════════════════════════════════════════════════════════════════════
773
774pub const SHOWCASE_13_NESTED_FOREACH: &str = r##"# ═══════════════════════════════════════════════════════════════════
775# Pattern 13: Nested For Each (categories x items)
776# ═══════════════════════════════════════════════════════════════════
777#
778# Outer for_each over categories, each producing items.
779# Inner task iterates over produced items. Simulates nested iteration
780# via chained for_each tasks.
781#
782# Prerequisites: LLM provider
783# Run: nika run workflows/showcase/13-nested-foreach.nika.yaml
784
785schema: "nika/workflow@0.12"
786workflow: nested-foreach
787description: "Nested iteration via chained for_each tasks"
788
789provider: "{{PROVIDER}}"
790model: "{{MODEL}}"
791
792tasks:
793  - id: generate_items
794    for_each: ["technology", "science", "art"]
795    as: category
796    concurrency: 3
797    structured:
798      schema:
799        type: object
800        properties:
801          category:
802            type: string
803          items:
804            type: array
805            items:
806              type: string
807            minItems: 2
808            maxItems: 3
809        required: [category, items]
810    infer:
811      prompt: |
812        For the category "{{with.category}}", generate 2-3 trending topic names.
813        Return the category and the list of items.
814      temperature: 0.7
815      max_tokens: 200
816
817  - id: describe_items
818    depends_on: [generate_items]
819    with:
820      categories: $generate_items
821    for_each: "{{with.categories}}"
822    as: cat_data
823    concurrency: 3
824    structured:
825      schema:
826        type: object
827        properties:
828          category:
829            type: string
830          descriptions:
831            type: array
832            items:
833              type: object
834              properties:
835                topic:
836                  type: string
837                description:
838                  type: string
839                  maxLength: 100
840              required: [topic, description]
841        required: [category, descriptions]
842    infer:
843      prompt: |
844        For each topic in this category, write a one-line description (max 100 chars).
845
846        Category data: {{with.cat_data}}
847      temperature: 0.5
848      max_tokens: 400
849
850  - id: final_report
851    depends_on: [describe_items]
852    with:
853      all_descriptions: $describe_items
854    exec:
855      command: |
856        echo 'Nested for_each complete: {{with.all_descriptions}}'
857      shell: true
858"##;
859
860// ═════════════════════════════════════════════════════════════════════════════
861// 14: Fan-Out Fan-In
862// ═════════════════════════════════════════════════════════════════════════════
863
864pub const SHOWCASE_14_FAN_OUT_FAN_IN: &str = r##"# ═══════════════════════════════════════════════════════════════════
865# Pattern 14: Fan-Out Fan-In
866# ═══════════════════════════════════════════════════════════════════
867#
868# Single source → for_each with 5 analyses → merge → synthesize.
869# The classic MapReduce pattern in a workflow.
870#
871# Prerequisites: LLM provider
872# Run: nika run workflows/showcase/14-fan-out-fan-in.nika.yaml
873
874schema: "nika/workflow@0.12"
875workflow: fan-out-fan-in
876description: "MapReduce pattern: fan-out analyses, fan-in synthesis"
877
878provider: "{{PROVIDER}}"
879model: "{{MODEL}}"
880
881tasks:
882  - id: source
883    exec:
884      command: |
885        echo 'Nika is a semantic YAML workflow engine for AI tasks. It supports 5 verbs (infer, exec, fetch, invoke, agent), a DAG scheduler for parallel execution, structured output with JSON Schema validation, and content-addressable storage for media. It connects to knowledge graphs via MCP protocol.'
886      shell: true
887
888  - id: analyze
889    depends_on: [source]
890    with:
891      text: $source
892    for_each:
893      - "technical_accuracy"
894      - "readability"
895      - "completeness"
896      - "market_positioning"
897      - "developer_appeal"
898    as: lens
899    concurrency: 5
900    structured:
901      schema:
902        type: object
903        properties:
904          lens:
905            type: string
906          score:
907            type: number
908            minimum: 0
909            maximum: 10
910          strengths:
911            type: array
912            items:
913              type: string
914            maxItems: 3
915          weaknesses:
916            type: array
917            items:
918              type: string
919            maxItems: 3
920          recommendation:
921            type: string
922            maxLength: 200
923        required: [lens, score, strengths, weaknesses, recommendation]
924    infer:
925      prompt: |
926        Analyze this product description through the lens of "{{with.lens}}".
927        Score 0-10, list up to 3 strengths and 3 weaknesses,
928        and give a recommendation (max 200 chars).
929
930        Text: "{{with.text}}"
931      temperature: 0.4
932      max_tokens: 400
933
934  - id: synthesize
935    depends_on: [analyze]
936    with:
937      analyses: $analyze
938    structured:
939      schema:
940        type: object
941        properties:
942          overall_score:
943            type: number
944            minimum: 0
945            maximum: 10
946          top_strength:
947            type: string
948          top_weakness:
949            type: string
950          action_items:
951            type: array
952            items:
953              type: string
954            minItems: 1
955            maxItems: 5
956          executive_summary:
957            type: string
958            maxLength: 300
959        required: [overall_score, top_strength, top_weakness, action_items, executive_summary]
960    infer:
961      prompt: |
962        Synthesize these 5 analyses into an executive summary.
963        Compute an overall score, identify the top strength and weakness,
964        and list 1-5 action items.
965
966        All analyses: {{with.analyses}}
967      temperature: 0.3
968      max_tokens: 500
969    artifact:
970      path: output/analysis/executive-summary.json
971      format: json
972"##;
973
974// ═════════════════════════════════════════════════════════════════════════════
975// 15: Pipeline with Retry
976// ═════════════════════════════════════════════════════════════════════════════
977
978pub const SHOWCASE_15_RETRY_PIPELINE: &str = r##"# ═══════════════════════════════════════════════════════════════════
979# Pattern 15: Pipeline with Retry
980# ═══════════════════════════════════════════════════════════════════
981#
982# for_each over tasks with retry on fetch failures + structured results.
983# Demonstrates: for_each + retry + fail_fast:false + structured output.
984#
985# Prerequisites: LLM provider (for summary)
986# Run: nika run workflows/showcase/15-retry-pipeline.nika.yaml
987
988schema: "nika/workflow@0.12"
989workflow: retry-pipeline
990description: "Resilient pipeline with retry on each iteration"
991
992provider: "{{PROVIDER}}"
993model: "{{MODEL}}"
994
995tasks:
996  - id: fetch_with_retry
997    for_each:
998      - "https://httpbin.org/get"
999      - "https://httpbin.org/uuid"
1000      - "https://httpbin.org/delay/1"
1001      - "https://httpbin.org/ip"
1002      - "https://httpbin.org/headers"
1003    as: api_url
1004    concurrency: 3
1005    fail_fast: false
1006    retry:
1007      max_attempts: 3
1008      delay_ms: 500
1009      backoff: 2.0
1010    fetch:
1011      url: "{{with.api_url}}"
1012      method: GET
1013      response: full
1014      timeout: 10
1015
1016  - id: summarize_results
1017    depends_on: [fetch_with_retry]
1018    with:
1019      raw: $fetch_with_retry
1020    structured:
1021      schema:
1022        type: object
1023        properties:
1024          total_requests:
1025            type: integer
1026          successful:
1027            type: integer
1028          failed:
1029            type: integer
1030          results:
1031            type: array
1032            items:
1033              type: object
1034              properties:
1035                url:
1036                  type: string
1037                status:
1038                  type: string
1039                  enum: ["success", "failed", "timeout"]
1040                attempts:
1041                  type: integer
1042                  minimum: 1
1043              required: [url, status, attempts]
1044        required: [total_requests, successful, failed, results]
1045      max_retries: 2
1046    infer:
1047      prompt: |
1048        Analyze these API call results. For each URL, report its status
1049        and how many attempts were needed. Include totals.
1050
1051        Raw results: {{with.raw}}
1052      temperature: 0.1
1053"##;
1054
1055// ═════════════════════════════════════════════════════════════════════════════
1056// Public API
1057// ═════════════════════════════════════════════════════════════════════════════
1058
1059/// Return all 15 showcase pattern workflows.
1060pub fn get_showcase_workflows() -> Vec<WorkflowTemplate> {
1061    vec![
1062        WorkflowTemplate {
1063            filename: "01-url-status.nika.yaml",
1064            tier_dir: "showcase",
1065            content: SHOWCASE_01_URL_STATUS,
1066        },
1067        WorkflowTemplate {
1068            filename: "02-lang-detect.nika.yaml",
1069            tier_dir: "showcase",
1070            content: SHOWCASE_02_LANG_DETECT,
1071        },
1072        WorkflowTemplate {
1073            filename: "03-sentiment.nika.yaml",
1074            tier_dir: "showcase",
1075            content: SHOWCASE_03_SENTIMENT,
1076        },
1077        WorkflowTemplate {
1078            filename: "04-translation.nika.yaml",
1079            tier_dir: "showcase",
1080            content: SHOWCASE_04_TRANSLATION,
1081        },
1082        WorkflowTemplate {
1083            filename: "05-api-tester.nika.yaml",
1084            tier_dir: "showcase",
1085            content: SHOWCASE_05_API_TESTER,
1086        },
1087        WorkflowTemplate {
1088            filename: "06-link-checker.nika.yaml",
1089            tier_dir: "showcase",
1090            content: SHOWCASE_06_LINK_CHECKER,
1091        },
1092        WorkflowTemplate {
1093            filename: "07-rss-aggregator.nika.yaml",
1094            tier_dir: "showcase",
1095            content: SHOWCASE_07_RSS_AGGREGATOR,
1096        },
1097        WorkflowTemplate {
1098            filename: "08-git-analyzer.nika.yaml",
1099            tier_dir: "showcase",
1100            content: SHOWCASE_08_GIT_ANALYZER,
1101        },
1102        WorkflowTemplate {
1103            filename: "09-pkg-versions.nika.yaml",
1104            tier_dir: "showcase",
1105            content: SHOWCASE_09_PKG_VERSIONS,
1106        },
1107        WorkflowTemplate {
1108            filename: "10-web-scraper.nika.yaml",
1109            tier_dir: "showcase",
1110            content: SHOWCASE_10_WEB_SCRAPER,
1111        },
1112        WorkflowTemplate {
1113            filename: "11-multi-format.nika.yaml",
1114            tier_dir: "showcase",
1115            content: SHOWCASE_11_MULTI_FORMAT,
1116        },
1117        WorkflowTemplate {
1118            filename: "12-image-analysis.nika.yaml",
1119            tier_dir: "showcase",
1120            content: SHOWCASE_12_IMAGE_ANALYSIS,
1121        },
1122        WorkflowTemplate {
1123            filename: "13-nested-foreach.nika.yaml",
1124            tier_dir: "showcase",
1125            content: SHOWCASE_13_NESTED_FOREACH,
1126        },
1127        WorkflowTemplate {
1128            filename: "14-fan-out-fan-in.nika.yaml",
1129            tier_dir: "showcase",
1130            content: SHOWCASE_14_FAN_OUT_FAN_IN,
1131        },
1132        WorkflowTemplate {
1133            filename: "15-retry-pipeline.nika.yaml",
1134            tier_dir: "showcase",
1135            content: SHOWCASE_15_RETRY_PIPELINE,
1136        },
1137    ]
1138}
1139
1140#[cfg(test)]
1141mod tests {
1142    use super::*;
1143
1144    #[test]
1145    fn test_showcase_workflow_count() {
1146        let workflows = get_showcase_workflows();
1147        assert_eq!(
1148            workflows.len(),
1149            15,
1150            "Should have exactly 15 showcase workflows"
1151        );
1152    }
1153
1154    #[test]
1155    fn test_showcase_filenames_unique() {
1156        let workflows = get_showcase_workflows();
1157        let mut names: Vec<&str> = workflows.iter().map(|w| w.filename).collect();
1158        let len = names.len();
1159        names.sort();
1160        names.dedup();
1161        assert_eq!(names.len(), len, "All filenames must be unique");
1162    }
1163
1164    #[test]
1165    fn test_showcase_all_have_schema() {
1166        let workflows = get_showcase_workflows();
1167        for w in &workflows {
1168            assert!(
1169                w.content.contains("schema: \"nika/workflow@0.12\""),
1170                "Workflow {} must declare schema",
1171                w.filename
1172            );
1173        }
1174    }
1175
1176    #[test]
1177    fn test_showcase_all_have_tasks() {
1178        let workflows = get_showcase_workflows();
1179        for w in &workflows {
1180            assert!(
1181                w.content.contains("tasks:"),
1182                "Workflow {} must have tasks section",
1183                w.filename
1184            );
1185        }
1186    }
1187
1188    #[test]
1189    fn test_showcase_all_nika_yaml_extension() {
1190        let workflows = get_showcase_workflows();
1191        for w in &workflows {
1192            assert!(
1193                w.filename.ends_with(".nika.yaml"),
1194                "Workflow {} must end with .nika.yaml",
1195                w.filename
1196            );
1197        }
1198    }
1199
1200    #[test]
1201    fn test_showcase_all_have_for_each() {
1202        let workflows = get_showcase_workflows();
1203        for w in &workflows {
1204            assert!(
1205                w.content.contains("for_each:"),
1206                "Workflow {} must use for_each (showcase pattern requirement)",
1207                w.filename
1208            );
1209        }
1210    }
1211
1212    #[test]
1213    fn test_showcase_structured_or_schema_present() {
1214        let workflows = get_showcase_workflows();
1215        let structured_count = workflows
1216            .iter()
1217            .filter(|w| w.content.contains("structured:"))
1218            .count();
1219        // At least 10 of the 15 must have structured output
1220        assert!(
1221            structured_count >= 10,
1222            "At least 10 workflows should use structured:, found {}",
1223            structured_count
1224        );
1225    }
1226
1227    #[test]
1228    fn test_showcase_concurrency_present() {
1229        let workflows = get_showcase_workflows();
1230        let concurrent_count = workflows
1231            .iter()
1232            .filter(|w| w.content.contains("concurrency:"))
1233            .count();
1234        assert!(
1235            concurrent_count >= 10,
1236            "At least 10 workflows should use concurrency:, found {}",
1237            concurrent_count
1238        );
1239    }
1240
1241    #[test]
1242    fn test_showcase_all_in_showcase_dir() {
1243        let workflows = get_showcase_workflows();
1244        for w in &workflows {
1245            assert_eq!(
1246                w.tier_dir, "showcase",
1247                "Workflow {} must be in showcase directory",
1248                w.filename
1249            );
1250        }
1251    }
1252
1253    #[test]
1254    fn test_showcase_valid_yaml() {
1255        let workflows = get_showcase_workflows();
1256        for w in &workflows {
1257            // Skip YAML validation for templates with placeholders
1258            if w.content.contains("{{PROVIDER}}") || w.content.contains("{{MODEL}}") {
1259                continue;
1260            }
1261            let parsed: Result<serde_json::Value, _> = serde_saphyr::from_str(w.content);
1262            assert!(
1263                parsed.is_ok(),
1264                "Workflow {} must be valid YAML: {:?}",
1265                w.filename,
1266                parsed.err()
1267            );
1268        }
1269    }
1270
1271    #[test]
1272    fn test_showcase_workflow_names_unique() {
1273        let workflows = get_showcase_workflows();
1274        let mut workflow_names: Vec<&str> = Vec::new();
1275        for w in &workflows {
1276            // Extract workflow: value
1277            for line in w.content.lines() {
1278                let trimmed = line.trim();
1279                if let Some(name) = trimmed.strip_prefix("workflow: ") {
1280                    workflow_names.push(name);
1281                }
1282            }
1283        }
1284        let len = workflow_names.len();
1285        workflow_names.sort();
1286        workflow_names.dedup();
1287        assert_eq!(
1288            workflow_names.len(),
1289            len,
1290            "All workflow: names must be unique"
1291        );
1292    }
1293}