Skip to main content

nika_init/
minimal.rs

1//! Minimal init workflows — 1 per verb
2//!
3//! Five workflows demonstrating the five Nika verbs:
4//! exec, fetch, infer, invoke, agent.
5//!
6//! These are generated by `nika init` as starter examples.
7//! LLM workflows use `{{PROVIDER}}` and `{{MODEL}}` placeholders
8//! that are replaced at generation time.
9
10use super::WorkflowTemplate;
11
12/// Exec verb — shell commands, env vars, piping
13pub const MINIMAL_EXEC: &str = r##"# ═══════════════════════════════════════════════════════════════════
14# exec: — Run shell commands
15# ═══════════════════════════════════════════════════════════════════
16#
17# The simplest Nika verb. No API key, no provider — just your shell.
18#
19# Run:  nika run workflows/01-exec.nika.yaml
20
21schema: "nika/workflow@0.12"
22workflow: exec-basics
23description: "Shell commands with exec:"
24
25tasks:
26  - id: hello
27    exec:
28      command: echo "Hello from Nika!"
29
30  - id: system_info
31    exec:
32      command: uname -a
33      timeout: 10
34
35  - id: count_files
36    depends_on: [hello]
37    exec:
38      command: ls -1 | wc -l
39      shell: true
40"##;
41
42/// Fetch verb — HTTP requests, JSON APIs
43pub const MINIMAL_FETCH: &str = r##"# ═══════════════════════════════════════════════════════════════════
44# fetch: — HTTP requests
45# ═══════════════════════════════════════════════════════════════════
46#
47# Make HTTP requests. No API key needed for public endpoints.
48#
49# Run:  nika run workflows/02-fetch.nika.yaml
50
51schema: "nika/workflow@0.12"
52workflow: fetch-basics
53description: "HTTP requests with fetch:"
54
55tasks:
56  - id: get_ip
57    fetch:
58      url: "https://httpbin.org/ip"
59      method: GET
60
61  - id: post_data
62    fetch:
63      url: "https://httpbin.org/post"
64      method: POST
65      headers:
66        Content-Type: "application/json"
67      body: '{"message": "Hello from Nika"}'
68
69  - id: show_result
70    depends_on: [get_ip]
71    with:
72      ip_data: $get_ip
73    exec:
74      command: echo "Your IP data — {{with.ip_data}}"
75"##;
76
77/// Infer verb — LLM generation
78pub const MINIMAL_INFER: &str = r##"# ═══════════════════════════════════════════════════════════════════
79# infer: — LLM generation
80# ═══════════════════════════════════════════════════════════════════
81#
82# Send prompts to an LLM. Requires a provider API key.
83#
84# Setup: nika provider set {{PROVIDER}}
85# Run:   nika run workflows/03-infer.nika.yaml
86
87schema: "nika/workflow@0.12"
88workflow: infer-basics
89description: "LLM prompts with infer:"
90
91tasks:
92  - id: haiku
93    infer:
94      model: "{{MODEL}}"
95      prompt: "Write a haiku about open source software."
96      temperature: 0.8
97      max_tokens: 100
98
99  - id: explain
100    infer:
101      model: "{{MODEL}}"
102      system: "You are a concise technical writer."
103      prompt: "Explain what a DAG is in 2 sentences."
104      temperature: 0.3
105      max_tokens: 200
106
107  - id: combine
108    depends_on: [haiku, explain]
109    with:
110      poem: $haiku
111      definition: $explain
112    infer:
113      model: "{{MODEL}}"
114      prompt: |
115        Combine these into a short blog post intro:
116
117        HAIKU:
118        {{with.poem}}
119
120        DAG DEFINITION:
121        {{with.definition}}
122      temperature: 0.5
123      max_tokens: 400
124"##;
125
126/// Invoke verb — MCP tool calls
127pub const MINIMAL_INVOKE: &str = r##"# ═══════════════════════════════════════════════════════════════════
128# invoke: — MCP tool calls
129# ═══════════════════════════════════════════════════════════════════
130#
131# Call builtin tools or external MCP servers.
132# Builtin tools (nika:*) need no setup.
133#
134# Run:  nika run workflows/04-invoke.nika.yaml
135
136schema: "nika/workflow@0.12"
137workflow: invoke-basics
138description: "Tool calls with invoke:"
139
140tasks:
141  - id: log_start
142    invoke:
143      tool: "nika:log"
144      params:
145        message: "Workflow started"
146        level: info
147
148  - id: emit_data
149    invoke:
150      tool: "nika:emit"
151      params:
152        key: "greeting"
153        value: "Hello from invoke!"
154
155  - id: assert_check
156    depends_on: [emit_data]
157    invoke:
158      tool: "nika:assert"
159      params:
160        condition: true
161        message: "Emit succeeded"
162"##;
163
164/// Agent verb — multi-turn LLM loop with tools
165pub const MINIMAL_AGENT: &str = r##"# ═══════════════════════════════════════════════════════════════════
166# agent: — Multi-turn LLM with tools
167# ═══════════════════════════════════════════════════════════════════
168#
169# An autonomous agent that can use tools in a loop.
170# Requires a provider API key.
171#
172# Setup: nika provider set {{PROVIDER}}
173# Run:   nika run workflows/05-agent.nika.yaml
174
175schema: "nika/workflow@0.12"
176workflow: agent-basics
177description: "Multi-turn agent with tools"
178
179tasks:
180  - id: scout
181    agent:
182      model: "{{MODEL}}"
183      system: |
184        You are a helpful file scout. List the files in the current
185        directory and describe what you find. Be concise.
186      prompt: "What files are in this project?"
187      tools:
188        - "nika:glob"
189        - "nika:read"
190      max_turns: 5
191      stop_condition: "answer_found"
192"##;
193
194/// Return minimal workflow templates (5 total, 1 per verb)
195pub fn get_minimal_workflows() -> Vec<WorkflowTemplate> {
196    vec![
197        WorkflowTemplate {
198            filename: "01-exec.nika.yaml",
199            tier_dir: "minimal",
200            content: MINIMAL_EXEC,
201        },
202        WorkflowTemplate {
203            filename: "02-fetch.nika.yaml",
204            tier_dir: "minimal",
205            content: MINIMAL_FETCH,
206        },
207        WorkflowTemplate {
208            filename: "03-infer.nika.yaml",
209            tier_dir: "minimal",
210            content: MINIMAL_INFER,
211        },
212        WorkflowTemplate {
213            filename: "04-invoke.nika.yaml",
214            tier_dir: "minimal",
215            content: MINIMAL_INVOKE,
216        },
217        WorkflowTemplate {
218            filename: "05-agent.nika.yaml",
219            tier_dir: "minimal",
220            content: MINIMAL_AGENT,
221        },
222    ]
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_minimal_workflow_count() {
231        let workflows = get_minimal_workflows();
232        assert_eq!(
233            workflows.len(),
234            5,
235            "Should have exactly 5 minimal workflows"
236        );
237    }
238
239    #[test]
240    fn test_minimal_filenames_unique() {
241        let workflows = get_minimal_workflows();
242        let mut names: Vec<&str> = workflows.iter().map(|w| w.filename).collect();
243        let len = names.len();
244        names.sort();
245        names.dedup();
246        assert_eq!(names.len(), len, "All filenames must be unique");
247    }
248
249    #[test]
250    fn test_minimal_all_have_schema() {
251        let workflows = get_minimal_workflows();
252        for w in &workflows {
253            assert!(
254                w.content.contains("schema: \"nika/workflow@0.12\""),
255                "Workflow {} must declare schema",
256                w.filename
257            );
258        }
259    }
260
261    #[test]
262    fn test_minimal_all_have_tasks() {
263        let workflows = get_minimal_workflows();
264        for w in &workflows {
265            assert!(
266                w.content.contains("tasks:"),
267                "Workflow {} must have tasks section",
268                w.filename
269            );
270        }
271    }
272
273    #[test]
274    fn test_minimal_all_nika_yaml_extension() {
275        let workflows = get_minimal_workflows();
276        for w in &workflows {
277            assert!(
278                w.filename.ends_with(".nika.yaml"),
279                "Workflow {} must end with .nika.yaml",
280                w.filename
281            );
282        }
283    }
284
285    #[test]
286    fn test_minimal_verbs_covered() {
287        let workflows = get_minimal_workflows();
288        let all_content: String = workflows.iter().map(|w| w.content).collect();
289        assert!(all_content.contains("exec:"), "Must cover exec: verb");
290        assert!(all_content.contains("fetch:"), "Must cover fetch: verb");
291        assert!(all_content.contains("infer:"), "Must cover infer: verb");
292        assert!(all_content.contains("invoke:"), "Must cover invoke: verb");
293        assert!(all_content.contains("agent:"), "Must cover agent: verb");
294    }
295
296    #[test]
297    fn test_minimal_valid_yaml() {
298        let workflows = get_minimal_workflows();
299        for w in &workflows {
300            // Skip YAML validation for templates with {{PROVIDER}} / {{MODEL}} placeholders
301            if w.content.contains("{{PROVIDER}}") || w.content.contains("{{MODEL}}") {
302                continue;
303            }
304            let parsed: Result<serde_json::Value, _> = serde_saphyr::from_str(w.content);
305            assert!(
306                parsed.is_ok(),
307                "Workflow {} must be valid YAML: {:?}",
308                w.filename,
309                parsed.err()
310            );
311        }
312    }
313}