@elyracode/workflows 0.7.9

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## [0.7.9] - 2026-05-23
4
+
5
+ ### Added
6
+ - Code-orchestrated workflow engine with `prompt`, `run`, `if`, and `parallel` step types
7
+ - `/workflow` command to list and execute workflows from `.elyra/workflows/`
8
+ - Template interpolation for passing data between steps (`{{steps.name.output}}`)
9
+ - `elyra-workflows` skill for teaching the agent about workflow patterns
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @elyracode/workflows
2
+
3
+ Code-orchestrated workflows for Elyra. Define multi-step pipelines where code handles the control flow and the LLM handles judgment — no token tax from LLM-based orchestration.
4
+
5
+ ## Why
6
+
7
+ Traditional agent orchestration uses one LLM to plan, spawn sub-agents, collect results, and decide next steps. Every sub-agent result re-enters the orchestrator's context window, paying a "token tax" that degrades quality as the window fills.
8
+
9
+ Workflows flip this: code orchestrates, the LLM only judges. Each step is focused, deterministic, and reproducible.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ elyra install npm:@elyracode/workflows
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ Create workflow files in `.elyra/workflows/`:
20
+
21
+ ```json
22
+ {
23
+ "name": "deploy",
24
+ "description": "Build, test, review, deploy",
25
+ "steps": [
26
+ { "name": "test", "run": "npm test" },
27
+ { "name": "review", "prompt": "Review test results:\n{{steps.test.output}}" },
28
+ { "name": "deploy", "run": "npm run deploy", "if": "{{steps.test.code}} == 0" }
29
+ ]
30
+ }
31
+ ```
32
+
33
+ Run with `/workflow deploy` or ask the agent to run it.
34
+
35
+ ## Step Types
36
+
37
+ - **`prompt`** — Send a focused prompt to the LLM
38
+ - **`run`** — Execute a shell command
39
+ - **`if`** — Conditional execution based on previous step results
40
+ - **`parallel`** — Run multiple steps concurrently
41
+
42
+ ## Template Variables
43
+
44
+ Reference previous step outputs with `{{steps.<name>.output}}`, `{{steps.<name>.code}}`, or `{{steps.<name>.stderr}}`.
@@ -0,0 +1,401 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { ExtensionAPI } from "@elyracode/coding-agent";
4
+ import { Type } from "typebox";
5
+
6
+ // ── Types ───────────────────────────────────────────────────────────────────
7
+
8
+ interface WorkflowStep {
9
+ name: string;
10
+ prompt?: string;
11
+ run?: string;
12
+ if?: string;
13
+ parallel?: WorkflowStep[];
14
+ }
15
+
16
+ interface WorkflowDefinition {
17
+ name: string;
18
+ description?: string;
19
+ steps: WorkflowStep[];
20
+ }
21
+
22
+ interface StepResult {
23
+ output: string;
24
+ stderr?: string;
25
+ code?: number;
26
+ skipped?: boolean;
27
+ type: "prompt" | "run" | "parallel" | "skipped";
28
+ }
29
+
30
+ interface WorkflowContext {
31
+ steps: Record<string, StepResult>;
32
+ }
33
+
34
+ // ── Helpers ─────────────────────────────────────────────────────────────────
35
+
36
+ function interpolate(template: string, ctx: WorkflowContext): string {
37
+ return template.replace(/\{\{steps\.(\w+)\.(\w+)\}\}/g, (_match, stepName: string, field: string) => {
38
+ const step = ctx.steps[stepName];
39
+ if (!step) return `(step "${stepName}" not found)`;
40
+ if (field === "output") return step.output ?? "";
41
+ if (field === "stderr") return step.stderr ?? "";
42
+ if (field === "code") return String(step.code ?? "");
43
+ return `(unknown field "${field}")`;
44
+ });
45
+ }
46
+
47
+ function evaluateCondition(condition: string, ctx: WorkflowContext): boolean {
48
+ const interpolated = interpolate(condition, ctx).trim();
49
+
50
+ // Simple equality: "value == expected"
51
+ const eqMatch = interpolated.match(/^(.+?)\s*==\s*(.+)$/);
52
+ if (eqMatch) {
53
+ return eqMatch[1].trim() === eqMatch[2].trim();
54
+ }
55
+
56
+ // Simple inequality: "value != expected"
57
+ const neqMatch = interpolated.match(/^(.+?)\s*!=\s*(.+)$/);
58
+ if (neqMatch) {
59
+ return neqMatch[1].trim() !== neqMatch[2].trim();
60
+ }
61
+
62
+ // Contains: 'text contains "word"'
63
+ const containsMatch = interpolated.match(/^(.+?)\s+contains\s+"(.+)"$/);
64
+ if (containsMatch) {
65
+ return containsMatch[1].includes(containsMatch[2]);
66
+ }
67
+
68
+ // Truthy: non-empty, non-zero
69
+ return interpolated !== "" && interpolated !== "0" && interpolated !== "false";
70
+ }
71
+
72
+ function loadWorkflows(workflowDir: string): WorkflowDefinition[] {
73
+ if (!existsSync(workflowDir)) return [];
74
+
75
+ const files = readdirSync(workflowDir).filter((f) => f.endsWith(".json"));
76
+ const workflows: WorkflowDefinition[] = [];
77
+
78
+ for (const file of files) {
79
+ try {
80
+ const content = readFileSync(join(workflowDir, file), "utf-8");
81
+ const parsed = JSON.parse(content) as WorkflowDefinition;
82
+ if (parsed.name && Array.isArray(parsed.steps)) {
83
+ workflows.push(parsed);
84
+ }
85
+ } catch {
86
+ // Skip invalid files
87
+ }
88
+ }
89
+
90
+ return workflows;
91
+ }
92
+
93
+ function formatStepResult(name: string, result: StepResult): string {
94
+ if (result.skipped) {
95
+ return `[${name}] skipped (condition not met)`;
96
+ }
97
+ if (result.type === "run") {
98
+ const status = result.code === 0 ? "ok" : `exit ${result.code}`;
99
+ const output = result.output ? `\n${result.output}` : "";
100
+ const stderr = result.stderr ? `\nstderr: ${result.stderr}` : "";
101
+ return `[${name}] run (${status})${output}${stderr}`;
102
+ }
103
+ if (result.type === "prompt") {
104
+ return `[${name}] prompt completed`;
105
+ }
106
+ if (result.type === "parallel") {
107
+ return `[${name}] parallel (${result.output})`;
108
+ }
109
+ return `[${name}] done`;
110
+ }
111
+
112
+ function truncate(text: string, maxLen: number): string {
113
+ if (text.length <= maxLen) return text;
114
+ return `${text.slice(0, maxLen)}... (truncated)`;
115
+ }
116
+
117
+ // ── Extension ───────────────────────────────────────────────────────────────
118
+
119
+ export default function (elyra: ExtensionAPI): void {
120
+ let cwd = "";
121
+ let workflows: WorkflowDefinition[] = [];
122
+
123
+ elyra.on("session_start", async (_event, ctx) => {
124
+ cwd = ctx.cwd;
125
+ const workflowDir = join(cwd, ".elyra", "workflows");
126
+ workflows = loadWorkflows(workflowDir);
127
+ });
128
+
129
+ // ── /workflow command ─────────────────────────────────────────────────
130
+
131
+ elyra.registerCommand("workflow", {
132
+ description: "Run a code-orchestrated workflow from .elyra/workflows/",
133
+ async handler(args, ctx) {
134
+ const workflowDir = join(cwd, ".elyra", "workflows");
135
+ workflows = loadWorkflows(workflowDir);
136
+
137
+ if (!args.trim()) {
138
+ // List available workflows
139
+ if (workflows.length === 0) {
140
+ ctx.ui.notify(
141
+ "No workflows found. Create JSON files in .elyra/workflows/",
142
+ "info",
143
+ );
144
+ return;
145
+ }
146
+ const list = workflows
147
+ .map((w) => ` ${w.name}${w.description ? ` — ${w.description}` : ""}`)
148
+ .join("\n");
149
+ ctx.ui.notify(`Available workflows:\n${list}\n\nRun with: /workflow <name>`, "info");
150
+ return;
151
+ }
152
+
153
+ const name = args.trim();
154
+ const workflow = workflows.find((w) => w.name === name);
155
+ if (!workflow) {
156
+ ctx.ui.notify(`Workflow "${name}" not found. Run /workflow to list available workflows.`, "error");
157
+ return;
158
+ }
159
+
160
+ // Execute the workflow
161
+ ctx.ui.notify(`Starting workflow: ${workflow.name}`, "info");
162
+ const context: WorkflowContext = { steps: {} };
163
+ const log: string[] = [];
164
+
165
+ for (const step of workflow.steps) {
166
+ // Check condition
167
+ if (step.if) {
168
+ const shouldRun = evaluateCondition(step.if, context);
169
+ if (!shouldRun) {
170
+ context.steps[step.name] = { output: "", skipped: true, type: "skipped" };
171
+ log.push(formatStepResult(step.name, context.steps[step.name]));
172
+ continue;
173
+ }
174
+ }
175
+
176
+ // Parallel steps
177
+ if (step.parallel && step.parallel.length > 0) {
178
+ ctx.ui.notify(`[${step.name}] running ${step.parallel.length} parallel steps...`, "info");
179
+ const results = await Promise.all(
180
+ step.parallel.map(async (subStep) => {
181
+ if (subStep.run) {
182
+ const cmd = interpolate(subStep.run, context);
183
+ const result = await elyra.exec("sh", ["-c", cmd], { timeout: 120_000, cwd });
184
+ return {
185
+ name: subStep.name,
186
+ result: {
187
+ output: result.stdout,
188
+ stderr: result.stderr,
189
+ code: result.code,
190
+ type: "run" as const,
191
+ },
192
+ };
193
+ }
194
+ return {
195
+ name: subStep.name,
196
+ result: { output: "", type: "run" as const },
197
+ };
198
+ }),
199
+ );
200
+ for (const r of results) {
201
+ context.steps[r.name] = r.result;
202
+ }
203
+ const summary = results.map((r) => `${r.name}: ${r.result.code === 0 ? "ok" : `exit ${r.result.code}`}`).join(", ");
204
+ context.steps[step.name] = { output: summary, type: "parallel" };
205
+ log.push(formatStepResult(step.name, context.steps[step.name]));
206
+ continue;
207
+ }
208
+
209
+ // Run step
210
+ if (step.run) {
211
+ const cmd = interpolate(step.run, context);
212
+ ctx.ui.notify(`[${step.name}] running: ${truncate(cmd, 80)}`, "info");
213
+ const result = await elyra.exec("sh", ["-c", cmd], { timeout: 120_000, cwd });
214
+ context.steps[step.name] = {
215
+ output: result.stdout,
216
+ stderr: result.stderr,
217
+ code: result.code,
218
+ type: "run",
219
+ };
220
+ log.push(formatStepResult(step.name, context.steps[step.name]));
221
+ continue;
222
+ }
223
+
224
+ // Prompt step
225
+ if (step.prompt) {
226
+ const prompt = interpolate(step.prompt, context);
227
+ ctx.ui.notify(`[${step.name}] sending prompt to agent...`, "info");
228
+ await ctx.sendUserMessage(prompt);
229
+ // The agent response will be in the session; capture it conceptually
230
+ context.steps[step.name] = {
231
+ output: "(agent responded in session)",
232
+ type: "prompt",
233
+ };
234
+ log.push(formatStepResult(step.name, context.steps[step.name]));
235
+ continue;
236
+ }
237
+ }
238
+
239
+ // Summary
240
+ const summary = [
241
+ `Workflow "${workflow.name}" completed (${workflow.steps.length} steps):`,
242
+ "",
243
+ ...log,
244
+ ].join("\n");
245
+ ctx.ui.notify(summary, "info");
246
+ },
247
+ });
248
+
249
+ // ── workflow_run tool ─────────────────────────────────────────────────
250
+
251
+ const runSchema = Type.Object({
252
+ name: Type.String({ description: "Name of the workflow to execute" }),
253
+ });
254
+
255
+ elyra.registerTool({
256
+ name: "workflow_run",
257
+ label: "Run Workflow",
258
+ description:
259
+ "Execute a code-orchestrated workflow from .elyra/workflows/. Workflows define multi-step pipelines where code handles control flow (sequencing, conditions, parallelism) and the LLM handles judgment (analysis, review, generation). Use this when the user asks to run a defined workflow.",
260
+ parameters: runSchema,
261
+ promptSnippet: "Run a workflow pipeline from .elyra/workflows/",
262
+ promptGuidelines: [
263
+ "List available workflows by reading .elyra/workflows/ directory",
264
+ "Suggest creating a workflow when the user describes a repeatable multi-step process",
265
+ ],
266
+ async execute(_toolCallId, params) {
267
+ const workflowDir = join(cwd, ".elyra", "workflows");
268
+ workflows = loadWorkflows(workflowDir);
269
+
270
+ const workflow = workflows.find((w) => w.name === params.name);
271
+ if (!workflow) {
272
+ const available = workflows.map((w) => w.name).join(", ") || "none";
273
+ return {
274
+ content: [{ type: "text", text: `Workflow "${params.name}" not found. Available: ${available}` }],
275
+ isError: true,
276
+ };
277
+ }
278
+
279
+ const context: WorkflowContext = { steps: {} };
280
+ const log: string[] = [`Executing workflow: ${workflow.name}`];
281
+
282
+ for (const step of workflow.steps) {
283
+ // Check condition
284
+ if (step.if) {
285
+ if (!evaluateCondition(step.if, context)) {
286
+ context.steps[step.name] = { output: "", skipped: true, type: "skipped" };
287
+ log.push(`[${step.name}] skipped`);
288
+ continue;
289
+ }
290
+ }
291
+
292
+ // Run step
293
+ if (step.run) {
294
+ const cmd = interpolate(step.run, context);
295
+ const result = await elyra.exec("sh", ["-c", cmd], { timeout: 120_000, cwd });
296
+ context.steps[step.name] = {
297
+ output: result.stdout,
298
+ stderr: result.stderr,
299
+ code: result.code,
300
+ type: "run",
301
+ };
302
+ const status = result.code === 0 ? "ok" : `exit ${result.code}`;
303
+ log.push(`[${step.name}] run (${status}): ${truncate(result.stdout, 200)}`);
304
+ continue;
305
+ }
306
+
307
+ // Prompt step — return the prompt for the agent to process
308
+ if (step.prompt) {
309
+ const prompt = interpolate(step.prompt, context);
310
+ context.steps[step.name] = { output: prompt, type: "prompt" };
311
+ log.push(`[${step.name}] prompt ready`);
312
+ // Return early — the agent should process this prompt
313
+ return {
314
+ content: [
315
+ {
316
+ type: "text",
317
+ text: [
318
+ ...log,
319
+ "",
320
+ `Step "${step.name}" requires LLM judgment. Process this prompt:`,
321
+ "",
322
+ prompt,
323
+ "",
324
+ `Remaining steps: ${workflow.steps
325
+ .slice(workflow.steps.indexOf(step) + 1)
326
+ .map((s) => s.name)
327
+ .join(", ") || "none"}`,
328
+ ].join("\n"),
329
+ },
330
+ ],
331
+ };
332
+ }
333
+
334
+ // Parallel
335
+ if (step.parallel) {
336
+ const results = await Promise.all(
337
+ step.parallel.map(async (sub) => {
338
+ if (sub.run) {
339
+ const cmd = interpolate(sub.run, context);
340
+ const r = await elyra.exec("sh", ["-c", cmd], { timeout: 120_000, cwd });
341
+ return { name: sub.name, output: r.stdout, code: r.code };
342
+ }
343
+ return { name: sub.name, output: "", code: 0 };
344
+ }),
345
+ );
346
+ for (const r of results) {
347
+ context.steps[r.name] = { output: r.output, code: r.code, type: "run" };
348
+ }
349
+ const summary = results.map((r) => `${r.name}: ${r.code === 0 ? "ok" : `exit ${r.code}`}`).join(", ");
350
+ context.steps[step.name] = { output: summary, type: "parallel" };
351
+ log.push(`[${step.name}] parallel: ${summary}`);
352
+ }
353
+ }
354
+
355
+ return {
356
+ content: [{ type: "text", text: log.join("\n") }],
357
+ };
358
+ },
359
+ });
360
+
361
+ // ── workflow_list tool ────────────────────────────────────────────────
362
+
363
+ const listSchema = Type.Object({});
364
+
365
+ elyra.registerTool({
366
+ name: "workflow_list",
367
+ label: "List Workflows",
368
+ description: "List available workflows defined in .elyra/workflows/. Shows each workflow name, description, and step count.",
369
+ parameters: listSchema,
370
+ promptSnippet: "List available workflow pipelines",
371
+ async execute() {
372
+ const workflowDir = join(cwd, ".elyra", "workflows");
373
+ workflows = loadWorkflows(workflowDir);
374
+
375
+ if (workflows.length === 0) {
376
+ return {
377
+ content: [
378
+ {
379
+ type: "text",
380
+ text: "No workflows found. Create JSON files in .elyra/workflows/ to define pipelines.\n\nExample workflow (.elyra/workflows/check.json):\n```json\n{\n \"name\": \"check\",\n \"description\": \"Lint, test, and type-check\",\n \"steps\": [\n { \"name\": \"lint\", \"run\": \"npm run lint\" },\n { \"name\": \"test\", \"run\": \"npm test\" },\n { \"name\": \"types\", \"run\": \"npm run check\" }\n ]\n}\n```",
381
+ },
382
+ ],
383
+ };
384
+ }
385
+
386
+ const lines = workflows.map((w) => {
387
+ const stepTypes = w.steps.map((s) => {
388
+ if (s.prompt) return "prompt";
389
+ if (s.run) return "run";
390
+ if (s.parallel) return "parallel";
391
+ return "?";
392
+ });
393
+ return `${w.name} — ${w.description ?? "(no description)"} (${w.steps.length} steps: ${stepTypes.join(", ")})`;
394
+ });
395
+
396
+ return {
397
+ content: [{ type: "text", text: `Available workflows:\n\n${lines.join("\n")}` }],
398
+ };
399
+ },
400
+ });
401
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@elyracode/workflows",
3
+ "version": "0.7.9",
4
+ "description": "Code-orchestrated workflows for Elyra -- define multi-step pipelines where code handles control flow and LLMs handle judgment",
5
+ "type": "module",
6
+ "keywords": [
7
+ "elyra-package",
8
+ "workflows",
9
+ "orchestration",
10
+ "pipelines",
11
+ "automation"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "Knut W. Horne",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/kwhorne/elyra.git",
18
+ "directory": "packages/workflows"
19
+ },
20
+ "elyra": {
21
+ "extensions": [
22
+ "./extensions/index.ts"
23
+ ],
24
+ "skills": [
25
+ "./skills"
26
+ ]
27
+ },
28
+ "peerDependencies": {
29
+ "@elyracode/coding-agent": "*",
30
+ "typebox": "*"
31
+ },
32
+ "scripts": {
33
+ "clean": "echo 'nothing to clean'",
34
+ "build": "echo 'nothing to build'",
35
+ "check": "echo 'nothing to check'"
36
+ }
37
+ }
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: elyra-workflows
3
+ description: Code-orchestrated workflows. Use when the user wants to define multi-step pipelines, automate sequences of tasks, or build reproducible processes that combine code execution with LLM judgment.
4
+ ---
5
+
6
+ # Workflows
7
+
8
+ ## When to Use
9
+
10
+ Use workflows when:
11
+ - The user wants to automate a multi-step process (build, test, deploy)
12
+ - A task requires both code execution and LLM judgment in sequence
13
+ - The user wants reproducible, deterministic pipelines
14
+ - Multiple steps need to pass data between each other
15
+
16
+ ## Workflow Files
17
+
18
+ Workflows are JSON files in `.elyra/workflows/`. Each file defines a named pipeline with ordered steps.
19
+
20
+ ## Step Types
21
+
22
+ | Type | Field | Description |
23
+ |------|-------|-------------|
24
+ | Prompt | `prompt` | Send a prompt to the LLM for judgment, analysis, or generation |
25
+ | Run | `run` | Execute a shell command |
26
+ | Conditional | `if` | Skip this step unless a condition is met |
27
+ | Parallel | `parallel` | Run multiple sub-steps concurrently |
28
+
29
+ ## Template Syntax
30
+
31
+ Use `{{steps.<name>.output}}` to reference output from a previous step. Available fields:
32
+ - `{{steps.<name>.output}}` — stdout or LLM response
33
+ - `{{steps.<name>.code}}` — exit code (run steps only)
34
+ - `{{steps.<name>.stderr}}` — stderr (run steps only)
35
+
36
+ ## Example Workflow
37
+
38
+ ```json
39
+ {
40
+ "name": "review-and-fix",
41
+ "description": "Find issues, fix them, verify the fix",
42
+ "steps": [
43
+ {
44
+ "name": "find-issues",
45
+ "prompt": "Run the linter and type checker, then list all errors"
46
+ },
47
+ {
48
+ "name": "fix",
49
+ "prompt": "Fix the issues found:\n{{steps.find-issues.output}}"
50
+ },
51
+ {
52
+ "name": "verify",
53
+ "run": "npm run check"
54
+ },
55
+ {
56
+ "name": "report",
57
+ "prompt": "Summarize what was fixed and whether verify passed:\n{{steps.verify.output}}"
58
+ }
59
+ ]
60
+ }
61
+ ```
62
+
63
+ ## Key Principle
64
+
65
+ Code handles orchestration. LLMs handle judgment. Each prompt step should be focused on a single task. Don't ask the LLM to plan — the workflow IS the plan.