@synergenius/flow-weaver 0.27.5 → 0.29.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.
@@ -46,12 +46,66 @@ export function generateInPlace(sourceCode, ast, options = {}) {
46
46
  path.resolve(nodeType.sourceLocation.file) !== path.resolve(ast.sourceFile)) {
47
47
  continue;
48
48
  }
49
+ // Skip built-in auto-injected nodes — step 1.2 handles their insertion
50
+ if (!nodeType.sourceLocation && nodeType.helperText != null) {
51
+ continue;
52
+ }
49
53
  const nodeTypeResult = replaceNodeTypeJSDoc(result, nodeType);
50
54
  if (nodeTypeResult.changed) {
51
55
  result = nodeTypeResult.code;
52
56
  hasChanges = true;
53
57
  }
54
58
  }
59
+ // Step 1.2: Insert built-in node functions that are auto-injected (no source in the file)
60
+ const usedNodeTypes = new Set(ast.instances.map((i) => i.nodeType));
61
+ const builtInNodes = ast.nodeTypes.filter((nt) => !nt.sourceLocation && nt.functionText && nt.helperText != null && usedNodeTypes.has(nt.name));
62
+ if (builtInNodes.length > 0) {
63
+ // Find insertion point: just before the workflow function's JSDoc
64
+ const workflowFnPattern = new RegExp(`(/\\*\\*[\\s\\S]*?@flowWeaver\\s+workflow[\\s\\S]*?\\*/)\\s*\\n\\s*(?:export\\s+)?(?:async\\s+)?function\\s+${ast.functionName}\\b`);
65
+ const match = result.match(workflowFnPattern);
66
+ if (match && match.index !== undefined) {
67
+ const insertionLines = [];
68
+ // Emit shared helpers once (deduplicated)
69
+ const emittedHelpers = new Set();
70
+ for (const node of builtInNodes) {
71
+ const helperText = production ? (node.helperTextProduction ?? null) : (node.helperText ?? null);
72
+ if (helperText && !emittedHelpers.has(helperText)) {
73
+ emittedHelpers.add(helperText);
74
+ insertionLines.push(helperText);
75
+ insertionLines.push('');
76
+ }
77
+ }
78
+ // Emit each built-in function with its JSDoc
79
+ for (const node of builtInNodes) {
80
+ const funcText = (production && node.functionTextProduction != null) ? node.functionTextProduction : node.functionText;
81
+ if (funcText) {
82
+ // Add JSDoc annotation so the function is recognized on re-parse
83
+ const portAnnotations = [];
84
+ portAnnotations.push('/**');
85
+ portAnnotations.push(` * @flowWeaver nodeType`);
86
+ for (const [name, port] of Object.entries(node.inputs)) {
87
+ if (name === 'execute')
88
+ continue;
89
+ const optPrefix = port.optional ? '[' : '';
90
+ const optSuffix = port.optional ? ']' : '';
91
+ portAnnotations.push(` * @input ${optPrefix}${name}${optSuffix} - ${port.label || name}`);
92
+ }
93
+ for (const [name, port] of Object.entries(node.outputs)) {
94
+ if (name === 'onSuccess' || name === 'onFailure')
95
+ continue;
96
+ portAnnotations.push(` * @output ${name} - ${port.label || name}`);
97
+ }
98
+ portAnnotations.push(' */');
99
+ insertionLines.push(portAnnotations.join('\n'));
100
+ insertionLines.push(funcText);
101
+ insertionLines.push('');
102
+ }
103
+ }
104
+ const insertionCode = insertionLines.join('\n');
105
+ result = result.slice(0, match.index) + insertionCode + '\n' + result.slice(match.index);
106
+ hasChanges = true;
107
+ }
108
+ }
55
109
  // Step 1.5: Remove orphaned nodeType functions (functions that don't match any AST nodeType)
56
110
  // When multi-workflow, consider ALL workflows' node types to avoid deleting types used by siblings
57
111
  const cleanupResult = removeOrphanedNodeTypeFunctions(result, ast, allWorkflows);
@@ -126,7 +126,9 @@ export function generateCode(ast, options) {
126
126
  // 2. relative file imports (sourceLocation file differs from workflow source)
127
127
  // 3. local nodes (same source file)
128
128
  const npmPackageNodes = ast.nodeTypes.filter((n) => n.importSource);
129
- const localNodes = ast.nodeTypes.filter((n) => !n.importSource && n.sourceLocation?.file === ast.sourceFile);
129
+ // Only include built-in nodes (no sourceLocation) that are actually used by this workflow
130
+ const referencedNodeTypes = new Set(ast.instances.map((i) => i.nodeType));
131
+ const localNodes = ast.nodeTypes.filter((n) => !n.importSource && (n.sourceLocation?.file === ast.sourceFile || (!n.sourceLocation && n.functionText && n.helperText != null && referencedNodeTypes.has(n.name))));
130
132
  const importedNodes = ast.nodeTypes.filter((n) => !n.importSource && n.sourceLocation?.file !== ast.sourceFile);
131
133
  if (importedNodes.length > 0 || npmPackageNodes.length > 0) {
132
134
  // Separate FUNCTION imports (from source) vs IMPORTED_WORKFLOW imports (from generated)
@@ -264,10 +266,26 @@ export function generateCode(ast, options) {
264
266
  if (inlineFunctions.length > 0) {
265
267
  lines.push('');
266
268
  addLine();
269
+ // Emit shared helper functions once (deduplicated across built-in nodes)
270
+ // In production mode, use helperTextProduction if set; if undefined, skip helpers entirely
271
+ // (production built-in nodes don't need mock helpers)
272
+ const emittedHelpers = new Set();
273
+ inlineFunctions.forEach((node) => {
274
+ if (node.importSource)
275
+ return;
276
+ const helperText = production ? (node.helperTextProduction ?? null) : (node.helperText ?? null);
277
+ if (helperText && !emittedHelpers.has(helperText)) {
278
+ emittedHelpers.add(helperText);
279
+ lines.push(helperText);
280
+ helperText.split('\n').forEach(() => addLine());
281
+ lines.push('');
282
+ addLine();
283
+ }
284
+ });
267
285
  inlineFunctions.forEach((node) => {
268
286
  if (node.importSource)
269
287
  return; // Never inline npm package functions
270
- const functionText = node.functionText;
288
+ const functionText = (production && node.functionTextProduction != null) ? node.functionTextProduction : node.functionText;
271
289
  if (functionText) {
272
290
  const functionWithoutDecorators = removeDecorators(functionText);
273
291
  // Add source mapping for the node function
@@ -299,7 +299,9 @@ export declare function findPath(ast: TWorkflowAST, fromNodeId: string, toNodeId
299
299
  * }
300
300
  * ```
301
301
  */
302
- export declare function getTopologicalOrder(ast: TWorkflowAST): string[];
302
+ export declare function getTopologicalOrder(ast: TWorkflowAST, options?: {
303
+ includeScopedChildren?: boolean;
304
+ }): string[];
303
305
  /**
304
306
  * Group nodes by execution level (nodes at same level can execute in parallel)
305
307
  *
package/dist/api/query.js CHANGED
@@ -521,7 +521,7 @@ export function findPath(ast, fromNodeId, toNodeId) {
521
521
  * }
522
522
  * ```
523
523
  */
524
- export function getTopologicalOrder(ast) {
524
+ export function getTopologicalOrder(ast, options) {
525
525
  const mainInstances = getMainFlowInstances(ast);
526
526
  const mainConnections = getMainFlowConnections(ast);
527
527
  const inDegree = new Map();
@@ -565,6 +565,30 @@ export function getTopologicalOrder(ast) {
565
565
  if (result.length !== mainInstances.length) {
566
566
  throw new Error('Cannot compute topological order: workflow contains cycles');
567
567
  }
568
+ // Optionally append scoped children (for debug: breakpoints need to know about them)
569
+ if (options?.includeScopedChildren) {
570
+ const scopedChildren = ast.instances.filter((inst) => isPerPortScopedChild(inst, ast, ast.nodeTypes));
571
+ // Append scoped children after their parent node in the result
572
+ const expanded = [];
573
+ const scopedByParent = new Map();
574
+ for (const child of scopedChildren) {
575
+ if (child.parent) {
576
+ const parentId = child.parent.id;
577
+ if (!scopedByParent.has(parentId)) {
578
+ scopedByParent.set(parentId, []);
579
+ }
580
+ scopedByParent.get(parentId).push(child.id);
581
+ }
582
+ }
583
+ for (const nodeId of result) {
584
+ expanded.push(nodeId);
585
+ const children = scopedByParent.get(nodeId);
586
+ if (children) {
587
+ expanded.push(...children);
588
+ }
589
+ }
590
+ return expanded;
591
+ }
568
592
  return result;
569
593
  }
570
594
  /**
@@ -259,8 +259,14 @@ export type TNodeTypeAST = {
259
259
  y?: number;
260
260
  /** Source code location */
261
261
  sourceLocation?: TSourceLocation;
262
- /** Original function source text */
262
+ /** Original function source text (main function only) */
263
263
  functionText?: string;
264
+ /** Production-optimized function source text (mock code stripped) */
265
+ functionTextProduction?: string;
266
+ /** Shared helper functions (mock helpers, utility functions) emitted once before main functions */
267
+ helperText?: string;
268
+ /** Production-optimized helper text */
269
+ helperTextProduction?: string;
264
270
  /** How success/failure branching works */
265
271
  branchingStrategy?: TBranchingStrategy;
266
272
  /** Field name for value-based branching */
@@ -0,0 +1,9 @@
1
+ /**
2
+ * DO NOT EDIT - generated by scripts/generate-built-in-registry.ts
3
+ *
4
+ * Registry of built-in node types for auto-injection into the parser.
5
+ * Generated from the actual source files in src/built-in-nodes/.
6
+ */
7
+ import type { TNodeTypeAST } from '../ast/types.js';
8
+ export declare const BUILT_IN_NODE_TYPES: TNodeTypeAST[];
9
+ //# sourceMappingURL=generated-registry.d.ts.map
@@ -0,0 +1,299 @@
1
+ /**
2
+ * DO NOT EDIT - generated by scripts/generate-built-in-registry.ts
3
+ *
4
+ * Registry of built-in node types for auto-injection into the parser.
5
+ * Generated from the actual source files in src/built-in-nodes/.
6
+ */
7
+ export const BUILT_IN_NODE_TYPES = [
8
+ {
9
+ type: 'NodeType',
10
+ name: 'delay',
11
+ functionName: 'delay',
12
+ isAsync: true,
13
+ hasSuccessPort: true,
14
+ hasFailurePort: true,
15
+ executeWhen: 'CONJUNCTION',
16
+ variant: 'FUNCTION',
17
+ inputs: {
18
+ execute: { dataType: 'STEP', label: 'Execute' },
19
+ duration: { dataType: 'STRING', label: 'Duration to sleep (e.g. "30s", "5m", "1h", "2d")', tsType: 'string' },
20
+ },
21
+ outputs: {
22
+ onSuccess: { dataType: 'STEP', label: 'On Success', isControlFlow: true },
23
+ onFailure: { dataType: 'STEP', label: 'On Failure', isControlFlow: true, failure: true },
24
+ elapsed: { dataType: 'BOOLEAN', label: 'Always true after sleep completes', tsType: 'boolean' },
25
+ },
26
+ helperText: `
27
+ function __fw_getMockConfig() {
28
+ return globalThis.__fw_mocks__;
29
+ }
30
+
31
+ function __fw_lookupMock(section, key) {
32
+ if (!section)
33
+ return undefined;
34
+ const nodeId = globalThis.__fw_current_node_id__;
35
+ if (nodeId) {
36
+ const qualified = section[\`\${nodeId}:\${key}\`];
37
+ if (qualified !== undefined)
38
+ return qualified;
39
+ }
40
+ return section[key];
41
+ }
42
+ `.trim(),
43
+ helperTextProduction: undefined,
44
+ functionText: `
45
+ async function delay(execute, duration) {
46
+ if (!execute)
47
+ return { onSuccess: false, onFailure: false, elapsed: false };
48
+ const mocks = __fw_getMockConfig();
49
+ if (mocks?.fast) {
50
+ await new Promise((resolve) => setTimeout(resolve, 1));
51
+ }
52
+ else {
53
+ const ms = __fw_parseDuration(duration);
54
+ await new Promise((resolve) => setTimeout(resolve, ms));
55
+ }
56
+ return { onSuccess: true, onFailure: false, elapsed: true };
57
+ }
58
+ function __fw_parseDuration(duration) {
59
+ const match = duration.match(/^(\\d+)(ms|s|m|h|d)$/);
60
+ if (!match)
61
+ return 0;
62
+ const [, value, unit] = match;
63
+ const multipliers = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
64
+ return parseInt(value) * (multipliers[unit] || 0);
65
+ }
66
+ `.trim(),
67
+ functionTextProduction: `
68
+ async function delay(execute, duration) {
69
+ if (!execute)
70
+ return { onSuccess: false, onFailure: false, elapsed: false };
71
+ const ms = __fw_parseDuration(duration);
72
+ await new Promise((resolve) => setTimeout(resolve, ms));
73
+ return { onSuccess: true, onFailure: false, elapsed: true };
74
+ }
75
+ function __fw_parseDuration(duration) {
76
+ const match = duration.match(/^(\\d+)(ms|s|m|h|d)$/);
77
+ if (!match)
78
+ return 0;
79
+ const [, value, unit] = match;
80
+ const multipliers = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
81
+ return parseInt(value) * (multipliers[unit] || 0);
82
+ }
83
+ `.trim(),
84
+ },
85
+ {
86
+ type: 'NodeType',
87
+ name: 'waitForEvent',
88
+ functionName: 'waitForEvent',
89
+ isAsync: true,
90
+ hasSuccessPort: true,
91
+ hasFailurePort: true,
92
+ executeWhen: 'CONJUNCTION',
93
+ variant: 'FUNCTION',
94
+ inputs: {
95
+ execute: { dataType: 'STEP', label: 'Execute' },
96
+ eventName: { dataType: 'STRING', label: 'Event name to wait for (e.g. "app/approval.received")', tsType: 'string' },
97
+ match: { dataType: 'STRING', label: 'Field to match between trigger and waited event (e.g. "data.requestId")', tsType: 'string', optional: true },
98
+ timeout: { dataType: 'STRING', label: 'Max wait time (e.g. "24h", "7d"). Empty = no timeout', tsType: 'string', optional: true },
99
+ },
100
+ outputs: {
101
+ onSuccess: { dataType: 'STEP', label: 'On Success', isControlFlow: true },
102
+ onFailure: { dataType: 'STEP', label: 'On Failure', isControlFlow: true, failure: true },
103
+ eventData: { dataType: 'OBJECT', label: 'The received event\'s data payload', tsType: 'object' },
104
+ },
105
+ helperText: `
106
+ function __fw_getMockConfig() {
107
+ return globalThis.__fw_mocks__;
108
+ }
109
+
110
+ function __fw_lookupMock(section, key) {
111
+ if (!section)
112
+ return undefined;
113
+ const nodeId = globalThis.__fw_current_node_id__;
114
+ if (nodeId) {
115
+ const qualified = section[\`\${nodeId}:\${key}\`];
116
+ if (qualified !== undefined)
117
+ return qualified;
118
+ }
119
+ return section[key];
120
+ }
121
+ `.trim(),
122
+ helperTextProduction: undefined,
123
+ functionText: `
124
+ async function waitForEvent(execute, eventName, match, timeout) {
125
+ if (!execute)
126
+ return { onSuccess: false, onFailure: false, eventData: {} };
127
+ const mocks = __fw_getMockConfig();
128
+ if (mocks) {
129
+ const mockData = __fw_lookupMock(mocks.events, eventName);
130
+ if (mockData !== undefined) {
131
+ return { onSuccess: true, onFailure: false, eventData: mockData };
132
+ }
133
+ return { onSuccess: false, onFailure: true, eventData: {} };
134
+ }
135
+ return { onSuccess: true, onFailure: false, eventData: {} };
136
+ }
137
+ `.trim(),
138
+ functionTextProduction: `
139
+ async function waitForEvent(execute, eventName, match, timeout) {
140
+ if (!execute)
141
+ return { onSuccess: false, onFailure: false, eventData: {} };
142
+ return { onSuccess: true, onFailure: false, eventData: {} };
143
+ }
144
+ `.trim(),
145
+ },
146
+ {
147
+ type: 'NodeType',
148
+ name: 'invokeWorkflow',
149
+ functionName: 'invokeWorkflow',
150
+ isAsync: true,
151
+ hasSuccessPort: true,
152
+ hasFailurePort: true,
153
+ executeWhen: 'CONJUNCTION',
154
+ variant: 'FUNCTION',
155
+ inputs: {
156
+ execute: { dataType: 'STEP', label: 'Execute' },
157
+ functionId: { dataType: 'STRING', label: 'Function ID of the workflow to invoke (e.g. "my-service/sub-workflow")', tsType: 'string' },
158
+ payload: { dataType: 'OBJECT', label: 'Data to pass as event.data to the invoked function', tsType: 'object' },
159
+ timeout: { dataType: 'STRING', label: 'Max wait time (e.g. "1h")', tsType: 'string', optional: true },
160
+ },
161
+ outputs: {
162
+ onSuccess: { dataType: 'STEP', label: 'On Success', isControlFlow: true },
163
+ onFailure: { dataType: 'STEP', label: 'On Failure', isControlFlow: true, failure: true },
164
+ result: { dataType: 'OBJECT', label: 'Return value from the invoked function', tsType: 'object' },
165
+ },
166
+ helperText: `
167
+ function __fw_getMockConfig() {
168
+ return globalThis.__fw_mocks__;
169
+ }
170
+
171
+ function __fw_lookupMock(section, key) {
172
+ if (!section)
173
+ return undefined;
174
+ const nodeId = globalThis.__fw_current_node_id__;
175
+ if (nodeId) {
176
+ const qualified = section[\`\${nodeId}:\${key}\`];
177
+ if (qualified !== undefined)
178
+ return qualified;
179
+ }
180
+ return section[key];
181
+ }
182
+ `.trim(),
183
+ helperTextProduction: undefined,
184
+ functionText: `
185
+ async function invokeWorkflow(execute, functionId, payload, timeout) {
186
+ if (!execute)
187
+ return { onSuccess: false, onFailure: false, result: {} };
188
+ const mocks = __fw_getMockConfig();
189
+ if (mocks) {
190
+ const mockResult = __fw_lookupMock(mocks.invocations, functionId);
191
+ if (mockResult !== undefined) {
192
+ return { onSuccess: true, onFailure: false, result: mockResult };
193
+ }
194
+ return { onSuccess: false, onFailure: true, result: {} };
195
+ }
196
+ const registry = globalThis.__fw_workflow_registry__;
197
+ if (registry?.[functionId]) {
198
+ try {
199
+ const result = await registry[functionId](true, payload);
200
+ return { onSuccess: true, onFailure: false, result: result ?? {} };
201
+ }
202
+ catch {
203
+ return { onSuccess: false, onFailure: true, result: {} };
204
+ }
205
+ }
206
+ return { onSuccess: true, onFailure: false, result: {} };
207
+ }
208
+ `.trim(),
209
+ functionTextProduction: `
210
+ async function invokeWorkflow(execute, functionId, payload, timeout) {
211
+ if (!execute)
212
+ return { onSuccess: false, onFailure: false, result: {} };
213
+ const registry = globalThis.__fw_workflow_registry__;
214
+ if (registry?.[functionId]) {
215
+ try {
216
+ const result = await registry[functionId](true, payload);
217
+ return { onSuccess: true, onFailure: false, result: result ?? {} };
218
+ }
219
+ catch {
220
+ return { onSuccess: false, onFailure: true, result: {} };
221
+ }
222
+ }
223
+ return { onSuccess: true, onFailure: false, result: {} };
224
+ }
225
+ `.trim(),
226
+ },
227
+ {
228
+ type: 'NodeType',
229
+ name: 'waitForAgent',
230
+ functionName: 'waitForAgent',
231
+ isAsync: true,
232
+ hasSuccessPort: true,
233
+ hasFailurePort: true,
234
+ executeWhen: 'CONJUNCTION',
235
+ variant: 'FUNCTION',
236
+ inputs: {
237
+ execute: { dataType: 'STEP', label: 'Execute' },
238
+ agentId: { dataType: 'STRING', label: 'Agent/task identifier', tsType: 'string' },
239
+ context: { dataType: 'OBJECT', label: 'Context data to send to the agent', tsType: 'object' },
240
+ prompt: { dataType: 'STRING', label: 'Message to display when requesting input', tsType: 'string', optional: true },
241
+ },
242
+ outputs: {
243
+ onSuccess: { dataType: 'STEP', label: 'On Success', isControlFlow: true },
244
+ onFailure: { dataType: 'STEP', label: 'On Failure', isControlFlow: true, failure: true },
245
+ agentResult: { dataType: 'OBJECT', label: 'Result returned by the agent', tsType: 'object' },
246
+ },
247
+ helperText: `
248
+ function __fw_getMockConfig() {
249
+ return globalThis.__fw_mocks__;
250
+ }
251
+
252
+ function __fw_lookupMock(section, key) {
253
+ if (!section)
254
+ return undefined;
255
+ const nodeId = globalThis.__fw_current_node_id__;
256
+ if (nodeId) {
257
+ const qualified = section[\`\${nodeId}:\${key}\`];
258
+ if (qualified !== undefined)
259
+ return qualified;
260
+ }
261
+ return section[key];
262
+ }
263
+ `.trim(),
264
+ helperTextProduction: undefined,
265
+ functionText: `
266
+ async function waitForAgent(execute, agentId, context, prompt) {
267
+ if (!execute)
268
+ return { onSuccess: false, onFailure: false, agentResult: {} };
269
+ const mocks = __fw_getMockConfig();
270
+ const mockResult = __fw_lookupMock(mocks?.agents, agentId);
271
+ if (mockResult !== undefined) {
272
+ return { onSuccess: true, onFailure: false, agentResult: mockResult };
273
+ }
274
+ if (mocks?.agents) {
275
+ return { onSuccess: false, onFailure: true, agentResult: {} };
276
+ }
277
+ const channel = globalThis.__fw_agent_channel__;
278
+ if (channel) {
279
+ const result = await channel.request({ agentId, context, prompt });
280
+ return { onSuccess: true, onFailure: false, agentResult: result };
281
+ }
282
+ return { onSuccess: true, onFailure: false, agentResult: {} };
283
+ }
284
+ `.trim(),
285
+ functionTextProduction: `
286
+ async function waitForAgent(execute, agentId, context, prompt) {
287
+ if (!execute)
288
+ return { onSuccess: false, onFailure: false, agentResult: {} };
289
+ const channel = globalThis.__fw_agent_channel__;
290
+ if (channel) {
291
+ const result = await channel.request({ agentId, context, prompt });
292
+ return { onSuccess: true, onFailure: false, agentResult: result };
293
+ }
294
+ return { onSuccess: true, onFailure: false, agentResult: {} };
295
+ }
296
+ `.trim(),
297
+ },
298
+ ];
299
+ //# sourceMappingURL=generated-registry.js.map
@@ -306,9 +306,9 @@ function printNocodeGuidance(_projectName) {
306
306
  logger.newline();
307
307
  logger.log(` ${logger.bold('Useful commands')}`);
308
308
  logger.newline();
309
- logger.log(` fw run src/*.ts ${logger.dim('Run your workflow')}`);
310
- logger.log(` fw diagram src/*.ts ${logger.dim('See a visual diagram')}`);
311
- logger.log(` fw mcp-setup ${logger.dim('Connect more AI editors')}`);
309
+ logger.log(` npx fw run src/*.ts ${logger.dim('Run your workflow')}`);
310
+ logger.log(` npx fw diagram src/*.ts ${logger.dim('See a visual diagram')}`);
311
+ logger.log(` npx fw mcp-setup ${logger.dim('Connect more AI editors')}`);
312
312
  }
313
313
  function printVibecoderGuidance() {
314
314
  logger.newline();
@@ -326,17 +326,17 @@ function printLowcodeGuidance() {
326
326
  logger.newline();
327
327
  logger.log(` ${logger.bold('Explore and customize')}`);
328
328
  logger.newline();
329
- logger.log(` fw templates ${logger.dim('List all 16 workflow templates')}`);
330
- logger.log(` fw describe src/*.ts ${logger.dim('See the workflow structure')}`);
331
- logger.log(` fw docs annotations ${logger.dim('Annotation reference')}`);
329
+ logger.log(` npx fw templates ${logger.dim('List all 16 workflow templates')}`);
330
+ logger.log(` npx fw describe src/*.ts ${logger.dim('See the workflow structure')}`);
331
+ logger.log(` npx fw docs annotations ${logger.dim('Annotation reference')}`);
332
332
  logger.newline();
333
333
  logger.log(` Your project includes an example in ${logger.highlight('examples/')} to study.`);
334
334
  logger.log(` With MCP connected, AI can help modify nodes and connections.`);
335
335
  }
336
336
  function printExpertGuidance() {
337
337
  logger.newline();
338
- logger.log(` fw mcp-setup ${logger.dim('Connect AI editors (Claude, Cursor, VS Code)')}`);
339
- logger.log(` fw docs ${logger.dim('Browse reference docs')}`);
338
+ logger.log(` npx fw mcp-setup ${logger.dim('Connect AI editors (Claude, Cursor, VS Code)')}`);
339
+ logger.log(` npx fw docs ${logger.dim('Browse reference docs')}`);
340
340
  }
341
341
  /** Pad a filename to align descriptions */
342
342
  function pad(displayName, width) {
@@ -350,16 +350,19 @@ export function generateProjectFiles(projectName, template, format = 'esm', pers
350
350
  '',
351
351
  `import { ${workflowName} } from './${workflowJsFile}';`,
352
352
  '',
353
- 'try {',
354
- ` const result = ${workflowName}(true, { data: { message: 'hello world' } });`,
355
- ' console.log(result);',
356
- '} catch (e) {',
353
+ 'async function main() {',
354
+ ` const result = await ${workflowName}(true, { data: { message: 'hello world' } });`,
355
+ ' console.log(JSON.stringify(result, null, 2));',
356
+ '}',
357
+ '',
358
+ 'main().catch((e) => {',
357
359
  " if (e instanceof Error && e.message.startsWith('Compile with:')) {",
358
360
  " console.error('Workflow not compiled yet. Run: npm run dev');",
359
361
  ' process.exit(1);',
360
362
  ' }',
361
- ' throw e;',
362
- '}',
363
+ ' console.error(e);',
364
+ ' process.exit(1);',
365
+ '});',
363
366
  '',
364
367
  ].join('\n');
365
368
  }
@@ -376,16 +379,19 @@ export function generateProjectFiles(projectName, template, format = 'esm', pers
376
379
  '',
377
380
  `const { ${workflowName} } = require('./${workflowJsFile}');`,
378
381
  '',
379
- 'try {',
380
- ` const result = ${workflowName}(true, { data: { message: 'hello world' } });`,
381
- ' console.log(result);',
382
- '} catch (e) {',
382
+ 'async function main() {',
383
+ ` const result = await ${workflowName}(true, { data: { message: 'hello world' } });`,
384
+ ' console.log(JSON.stringify(result, null, 2));',
385
+ '}',
386
+ '',
387
+ 'main().catch((e) => {',
383
388
  " if (e instanceof Error && e.message.startsWith('Compile with:')) {",
384
389
  " console.error('Workflow not compiled yet. Run: npm run dev');",
385
390
  ' process.exit(1);',
386
391
  ' }',
387
- ' throw e;',
388
- '}',
392
+ ' console.error(e);',
393
+ ' process.exit(1);',
394
+ '});',
389
395
  '',
390
396
  ].join('\n');
391
397
  }
@@ -339,6 +339,12 @@ async function runCommandInner(input, options) {
339
339
  logger.newline();
340
340
  logger.section('Result');
341
341
  logger.log(JSON.stringify(result.result, null, 2));
342
+ // Hint when the workflow failed and no params were provided
343
+ const resultObj = result.result;
344
+ if (resultObj?.onFailure === true && !options.params && !options.paramsFile) {
345
+ logger.newline();
346
+ logger.warn('Tip: use --params to provide input. Run `fw describe <file>` to see expected inputs.');
347
+ }
342
348
  // Show trace summary only when --trace is explicitly set (not on --stream, which already printed live)
343
349
  if (options.trace && !options.stream && result.trace && result.trace.length > 0) {
344
350
  logger.newline();