@pikku/inspector 0.12.33 → 0.12.34

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 CHANGED
@@ -1,3 +1,19 @@
1
+ ## 0.12.34
2
+
3
+ ### Patch Changes
4
+
5
+ - 92bd643: User flows in the console: workflow graph extraction now captures
6
+ `workflow.expectEventually` steps and per-step actor names (`{ actor:
7
+ actors.x }`), workflow meta carries `actors`/`title` into the serialized
8
+ graph, the CLI emits `user-flow-actors.gen.json` for the new
9
+ `MetaService.getUserFlowActorsMeta()`, and the console Workflows page gains a
10
+ Workflows / User Flows / Personas toggle. Also fixes complex-workflow graphs
11
+ being clobbered by a duplicate basic-extraction pass after successful DSL
12
+ extraction.
13
+ - Updated dependencies [35a9bab]
14
+ - Updated dependencies [92bd643]
15
+ - @pikku/core@0.12.50
16
+
1
17
  ## 0.12.33
2
18
 
3
19
  ### Patch Changes
@@ -5,7 +5,7 @@ import { ErrorCode } from '../error-codes.js';
5
5
  import { extractStringLiteral, isStringLike, isFunctionLike, extractDescription, extractDuration, } from '../utils/extract-node-value.js';
6
6
  import { getCommonWireMetaData, getPropertyValue, } from '../utils/get-property-value.js';
7
7
  import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js';
8
- import { getSourceText } from '../utils/workflow/dsl/patterns.js';
8
+ import { getSourceText, extractActorFromOptions, } from '../utils/workflow/dsl/patterns.js';
9
9
  /**
10
10
  * Extract a workflow step's display name without letting a non-static name
11
11
  * (e.g. a function call) abort the scan. The step name is cosmetic, so a
@@ -86,14 +86,18 @@ export function collectInvokedRPCs(steps, rpcs) {
86
86
  }
87
87
  }
88
88
  /**
89
- * Scan for workflow.do(), workflow.sleep(), and workflow.cancel() calls to extract workflow steps
89
+ * Scan for workflow.do(), workflow.expectEventually(), workflow.sleep(), and
90
+ * workflow.cancel() calls to extract workflow steps
90
91
  */
91
92
  function getWorkflowInvocations(node, checker, state, workflowName, steps) {
92
93
  // Look for property access expressions: workflow.do or workflow.sleep
93
94
  if (ts.isPropertyAccessExpression(node)) {
94
95
  const { name } = node;
95
96
  // Check if this is accessing 'do' or 'sleep' property
96
- if (name.text === 'do' || name.text === 'sleep' || name.text === 'cancel') {
97
+ if (name.text === 'do' ||
98
+ name.text === 'sleep' ||
99
+ name.text === 'cancel' ||
100
+ name.text === 'expectEventually') {
97
101
  // Check if the parent is a call expression
98
102
  const parent = node.parent;
99
103
  if (ts.isCallExpression(parent) && parent.expression === node) {
@@ -113,6 +117,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
113
117
  type: 'rpc',
114
118
  stepName,
115
119
  rpcName,
120
+ actor: extractActorFromOptions(optionsArg),
116
121
  });
117
122
  state.rpc.invokedFunctions.add(rpcName);
118
123
  }
@@ -125,6 +130,21 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
125
130
  });
126
131
  }
127
132
  }
133
+ else if (name.text === 'expectEventually' && args.length >= 4) {
134
+ // workflow.expectEventually(stepName, rpcName, data, predicate, options?)
135
+ const stepName = extractStepName(args[0], checker);
136
+ if (isStringLike(args[1], checker)) {
137
+ const rpcName = extractStringLiteral(args[1], checker);
138
+ steps.push({
139
+ type: 'rpc',
140
+ stepName,
141
+ rpcName,
142
+ expectEventually: true,
143
+ actor: extractActorFromOptions(args.length >= 5 ? args[4] : undefined),
144
+ });
145
+ state.rpc.invokedFunctions.add(rpcName);
146
+ }
147
+ }
128
148
  else if (name.text === 'sleep' && args.length >= 2) {
129
149
  // workflow.sleep(stepName, duration)
130
150
  const stepNameArg = args[0];
@@ -282,9 +302,19 @@ export const addWorkflow = (logger, node, checker, state) => {
282
302
  // For pikkuWorkflowComplexFunc, also run basic extraction so RPCs in
283
303
  // patterns the DSL extractor doesn't handle (array+push, nested Promise.all
284
304
  // with identifier args, etc.) are still registered as invoked functions.
305
+ // When DSL extraction already produced steps this pass is registration-only:
306
+ // appending to `steps` would duplicate nodes and clobber the graph.
285
307
  if (wrapperType !== 'dsl') {
286
- getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps);
308
+ const sink = steps.length > 0 ? [] : steps;
309
+ getWorkflowInvocations(resolvedFunc, checker, state, workflowName, sink);
287
310
  }
311
+ // Actor names the flow's steps run as (user flows) — powers the console
312
+ // personas view and the User Flow graph badges.
313
+ const actorNames = [
314
+ ...new Set(steps
315
+ .map((s) => ('actor' in s ? s.actor : undefined))
316
+ .filter((a) => typeof a === 'string')),
317
+ ];
288
318
  state.workflows.meta[workflowName] = {
289
319
  pikkuFuncId,
290
320
  name: workflowName,
@@ -298,6 +328,7 @@ export const addWorkflow = (logger, node, checker, state) => {
298
328
  tags,
299
329
  expose,
300
330
  userFlow: wrapperType === 'user-flow' ? true : undefined,
331
+ actors: actorNames.length > 0 ? actorNames : undefined,
301
332
  };
302
333
  // User flows are pure stories of remote RPCs (same rule as client-side CLI
303
334
  // renderers): the func may only destructure logger/config — everything else
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript';
2
- import { isWorkflowDoCall, isWorkflowSleepCall, isWorkflowSuspendCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
2
+ import { isWorkflowDoCall, isWorkflowExpectEventuallyCall, extractActorFromOptions, isWorkflowSleepCall, isWorkflowSuspendCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
3
3
  import { validateNoDisallowedPatterns, validateAwaitedCalls, formatValidationErrors, } from './validation.js';
4
4
  import { extractStringLiteral, extractNumberLiteral, } from '../../extract-node-value.js';
5
5
  /**
@@ -386,9 +386,12 @@ function extractRpcStep(call, context, outputVar) {
386
386
  const rpcName = extractStringLiteral(args[1], context.checker);
387
387
  // Extract inputs from third argument
388
388
  const inputs = args.length >= 3 ? extractInputSources(args[2], context) : undefined;
389
- // Extract options from fourth argument
390
- const options = args.length >= 4 && ts.isObjectLiteralExpression(args[3])
391
- ? extractStepOptions(args[3], context)
389
+ // do(step, rpc, data, options?) vs expectEventually(step, rpc, data, predicate, options?)
390
+ const expectEventually = isWorkflowExpectEventuallyCall(call);
391
+ const optionsIndex = expectEventually ? 4 : 3;
392
+ const optionsArg = args.length > optionsIndex ? args[optionsIndex] : undefined;
393
+ const options = optionsArg && ts.isObjectLiteralExpression(optionsArg)
394
+ ? extractStepOptions(optionsArg, context)
392
395
  : undefined;
393
396
  return {
394
397
  type: 'rpc',
@@ -397,6 +400,8 @@ function extractRpcStep(call, context, outputVar) {
397
400
  outputVar,
398
401
  inputs,
399
402
  options,
403
+ actor: extractActorFromOptions(optionsArg),
404
+ expectEventually: expectEventually || undefined,
400
405
  };
401
406
  }
402
407
  catch (error) {
@@ -3,9 +3,20 @@ import * as ts from 'typescript';
3
3
  * Pattern detection helpers for DSL workflow extraction
4
4
  */
5
5
  /**
6
- * Check if a call expression is workflow.do()
6
+ * Check if a call expression is workflow.do() or workflow.expectEventually()
7
+ * (both are RPC steps; expectEventually is the polling variant used by user flows)
7
8
  */
8
9
  export declare function isWorkflowDoCall(node: ts.CallExpression, checker: ts.TypeChecker): boolean;
10
+ /**
11
+ * Check if a call expression is workflow.expectEventually()
12
+ */
13
+ export declare function isWorkflowExpectEventuallyCall(node: ts.CallExpression): boolean;
14
+ /**
15
+ * Extract the actor NAME from a step options object literal:
16
+ * `{ actor: actors.x }` → 'x' (also `actors['x']`). Undefined when absent
17
+ * or not statically resolvable.
18
+ */
19
+ export declare function extractActorFromOptions(optionsArg: ts.Expression | undefined): string | undefined;
9
20
  /**
10
21
  * Check if a call expression is workflow.sleep()
11
22
  */
@@ -3,17 +3,52 @@ import * as ts from 'typescript';
3
3
  * Pattern detection helpers for DSL workflow extraction
4
4
  */
5
5
  /**
6
- * Check if a call expression is workflow.do()
6
+ * Check if a call expression is workflow.do() or workflow.expectEventually()
7
+ * (both are RPC steps; expectEventually is the polling variant used by user flows)
7
8
  */
8
9
  export function isWorkflowDoCall(node, checker) {
9
10
  if (!ts.isPropertyAccessExpression(node.expression)) {
10
11
  return false;
11
12
  }
12
13
  const propAccess = node.expression;
13
- return (propAccess.name.text === 'do' &&
14
+ return ((propAccess.name.text === 'do' ||
15
+ propAccess.name.text === 'expectEventually') &&
14
16
  ts.isIdentifier(propAccess.expression) &&
15
17
  propAccess.expression.text === 'workflow');
16
18
  }
19
+ /**
20
+ * Check if a call expression is workflow.expectEventually()
21
+ */
22
+ export function isWorkflowExpectEventuallyCall(node) {
23
+ return (ts.isPropertyAccessExpression(node.expression) &&
24
+ node.expression.name.text === 'expectEventually' &&
25
+ ts.isIdentifier(node.expression.expression) &&
26
+ node.expression.expression.text === 'workflow');
27
+ }
28
+ /**
29
+ * Extract the actor NAME from a step options object literal:
30
+ * `{ actor: actors.x }` → 'x' (also `actors['x']`). Undefined when absent
31
+ * or not statically resolvable.
32
+ */
33
+ export function extractActorFromOptions(optionsArg) {
34
+ if (!optionsArg || !ts.isObjectLiteralExpression(optionsArg))
35
+ return;
36
+ for (const prop of optionsArg.properties) {
37
+ if (ts.isPropertyAssignment(prop) &&
38
+ ts.isIdentifier(prop.name) &&
39
+ prop.name.text === 'actor') {
40
+ const init = prop.initializer;
41
+ if (ts.isPropertyAccessExpression(init)) {
42
+ return init.name.text;
43
+ }
44
+ if (ts.isElementAccessExpression(init) &&
45
+ ts.isStringLiteral(init.argumentExpression)) {
46
+ return init.argumentExpression.text;
47
+ }
48
+ }
49
+ }
50
+ return;
51
+ }
17
52
  /**
18
53
  * Check if a call expression is workflow.sleep()
19
54
  */
@@ -58,6 +58,12 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
58
58
  rpcName: step.rpcName,
59
59
  next: nextNodeId,
60
60
  };
61
+ if (step.actor) {
62
+ node.actor = step.actor;
63
+ }
64
+ if (step.expectEventually) {
65
+ node.expectEventually = true;
66
+ }
61
67
  if (step.inputs) {
62
68
  if (step.inputs === 'passthrough') {
63
69
  // Entire data is passed through - store as reference to trigger
@@ -304,8 +310,10 @@ export function convertDslToGraph(workflowName, meta) {
304
310
  name: workflowName,
305
311
  pikkuFuncId: meta.pikkuFuncId,
306
312
  source,
313
+ title: meta.title,
307
314
  description: meta.description,
308
315
  tags: meta.tags,
316
+ actors: meta.actors,
309
317
  context: meta.context,
310
318
  nodes: nodesRecord,
311
319
  entryNodeIds,
@@ -96,6 +96,10 @@ export interface FunctionNode extends BaseNode {
96
96
  outputVar?: string;
97
97
  /** Hash of nodeId + RPC input/output schemas for version detection */
98
98
  stepHash?: string;
99
+ /** User-flow actor name this step runs as (workflow.do {actor: actors.x}) */
100
+ actor?: string;
101
+ /** True for workflow.expectEventually polling steps (user flows) */
102
+ expectEventually?: boolean;
99
103
  }
100
104
  /**
101
105
  * Flow node - control flow only, no RPC call
@@ -136,10 +140,14 @@ export interface SerializedWorkflowGraph {
136
140
  pikkuFuncId: string;
137
141
  /** Source type: 'dsl' for pikkuWorkflowFunc, 'graph' for pikkuWorkflowGraph */
138
142
  source: WorkflowSourceType;
143
+ /** Optional short display name */
144
+ title?: string;
139
145
  /** Optional description */
140
146
  description?: string;
141
147
  /** Tags for organization */
142
148
  tags?: string[];
149
+ /** Actor names a user flow's steps run as */
150
+ actors?: string[];
143
151
  /** If true, workflow always executes inline without queues */
144
152
  inline?: boolean;
145
153
  /** Workflow context/state variables (from Zod schema) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.33",
3
+ "version": "0.12.34",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
38
- "@pikku/core": "^0.12.49",
38
+ "@pikku/core": "^0.12.50",
39
39
  "openapi-types": "^12.1.3",
40
40
  "path-to-regexp": "^8.3.0",
41
41
  "ts-json-schema-generator": "2.9.1-next.16",
@@ -16,7 +16,10 @@ import {
16
16
  getPropertyValue,
17
17
  } from '../utils/get-property-value.js'
18
18
  import { extractDSLWorkflow } from '../utils/workflow/dsl/extract-dsl-workflow.js'
19
- import { getSourceText } from '../utils/workflow/dsl/patterns.js'
19
+ import {
20
+ getSourceText,
21
+ extractActorFromOptions,
22
+ } from '../utils/workflow/dsl/patterns.js'
20
23
 
21
24
  /**
22
25
  * Extract a workflow step's display name without letting a non-static name
@@ -86,7 +89,8 @@ export function collectInvokedRPCs(
86
89
  }
87
90
 
88
91
  /**
89
- * Scan for workflow.do(), workflow.sleep(), and workflow.cancel() calls to extract workflow steps
92
+ * Scan for workflow.do(), workflow.expectEventually(), workflow.sleep(), and
93
+ * workflow.cancel() calls to extract workflow steps
90
94
  */
91
95
  function getWorkflowInvocations(
92
96
  node: ts.Node,
@@ -100,7 +104,12 @@ function getWorkflowInvocations(
100
104
  const { name } = node
101
105
 
102
106
  // Check if this is accessing 'do' or 'sleep' property
103
- if (name.text === 'do' || name.text === 'sleep' || name.text === 'cancel') {
107
+ if (
108
+ name.text === 'do' ||
109
+ name.text === 'sleep' ||
110
+ name.text === 'cancel' ||
111
+ name.text === 'expectEventually'
112
+ ) {
104
113
  // Check if the parent is a call expression
105
114
  const parent = node.parent
106
115
  if (ts.isCallExpression(parent) && parent.expression === node) {
@@ -125,6 +134,7 @@ function getWorkflowInvocations(
125
134
  type: 'rpc',
126
135
  stepName,
127
136
  rpcName,
137
+ actor: extractActorFromOptions(optionsArg),
128
138
  })
129
139
  state.rpc.invokedFunctions.add(rpcName)
130
140
  } else if (isFunctionLike(secondArg)) {
@@ -135,6 +145,20 @@ function getWorkflowInvocations(
135
145
  description: description || '<dynamic>',
136
146
  })
137
147
  }
148
+ } else if (name.text === 'expectEventually' && args.length >= 4) {
149
+ // workflow.expectEventually(stepName, rpcName, data, predicate, options?)
150
+ const stepName = extractStepName(args[0], checker)
151
+ if (isStringLike(args[1], checker)) {
152
+ const rpcName = extractStringLiteral(args[1], checker)
153
+ steps.push({
154
+ type: 'rpc',
155
+ stepName,
156
+ rpcName,
157
+ expectEventually: true,
158
+ actor: extractActorFromOptions(args.length >= 5 ? args[4] : undefined),
159
+ })
160
+ state.rpc.invokedFunctions.add(rpcName)
161
+ }
138
162
  } else if (name.text === 'sleep' && args.length >= 2) {
139
163
  // workflow.sleep(stepName, duration)
140
164
  const stepNameArg = args[0]
@@ -337,10 +361,23 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
337
361
  // For pikkuWorkflowComplexFunc, also run basic extraction so RPCs in
338
362
  // patterns the DSL extractor doesn't handle (array+push, nested Promise.all
339
363
  // with identifier args, etc.) are still registered as invoked functions.
364
+ // When DSL extraction already produced steps this pass is registration-only:
365
+ // appending to `steps` would duplicate nodes and clobber the graph.
340
366
  if (wrapperType !== 'dsl') {
341
- getWorkflowInvocations(resolvedFunc, checker, state, workflowName, steps)
367
+ const sink: WorkflowStepMeta[] = steps.length > 0 ? [] : steps
368
+ getWorkflowInvocations(resolvedFunc, checker, state, workflowName, sink)
342
369
  }
343
370
 
371
+ // Actor names the flow's steps run as (user flows) — powers the console
372
+ // personas view and the User Flow graph badges.
373
+ const actorNames = [
374
+ ...new Set(
375
+ steps
376
+ .map((s) => ('actor' in s ? s.actor : undefined))
377
+ .filter((a): a is string => typeof a === 'string')
378
+ ),
379
+ ]
380
+
344
381
  state.workflows.meta[workflowName] = {
345
382
  pikkuFuncId,
346
383
  name: workflowName,
@@ -354,6 +391,7 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
354
391
  tags,
355
392
  expose,
356
393
  userFlow: wrapperType === 'user-flow' ? true : undefined,
394
+ actors: actorNames.length > 0 ? actorNames : undefined,
357
395
  }
358
396
 
359
397
  // User flows are pure stories of remote RPCs (same rule as client-side CLI
@@ -21,6 +21,8 @@ import type {
21
21
  } from '@pikku/core/workflow'
22
22
  import {
23
23
  isWorkflowDoCall,
24
+ isWorkflowExpectEventuallyCall,
25
+ extractActorFromOptions,
24
26
  isWorkflowSleepCall,
25
27
  isWorkflowSuspendCall,
26
28
  isThrowCancelException,
@@ -553,10 +555,15 @@ function extractRpcStep(
553
555
  const inputs =
554
556
  args.length >= 3 ? extractInputSources(args[2], context) : undefined
555
557
 
556
- // Extract options from fourth argument
558
+ // do(step, rpc, data, options?) vs expectEventually(step, rpc, data, predicate, options?)
559
+ const expectEventually = isWorkflowExpectEventuallyCall(call)
560
+ const optionsIndex = expectEventually ? 4 : 3
561
+ const optionsArg =
562
+ args.length > optionsIndex ? args[optionsIndex] : undefined
563
+
557
564
  const options =
558
- args.length >= 4 && ts.isObjectLiteralExpression(args[3])
559
- ? extractStepOptions(args[3], context)
565
+ optionsArg && ts.isObjectLiteralExpression(optionsArg)
566
+ ? extractStepOptions(optionsArg, context)
560
567
  : undefined
561
568
 
562
569
  return {
@@ -566,6 +573,8 @@ function extractRpcStep(
566
573
  outputVar,
567
574
  inputs,
568
575
  options,
576
+ actor: extractActorFromOptions(optionsArg),
577
+ expectEventually: expectEventually || undefined,
569
578
  }
570
579
  } catch (error) {
571
580
  context.errors.push({
@@ -5,7 +5,8 @@ import * as ts from 'typescript'
5
5
  */
6
6
 
7
7
  /**
8
- * Check if a call expression is workflow.do()
8
+ * Check if a call expression is workflow.do() or workflow.expectEventually()
9
+ * (both are RPC steps; expectEventually is the polling variant used by user flows)
9
10
  */
10
11
  export function isWorkflowDoCall(
11
12
  node: ts.CallExpression,
@@ -17,12 +18,57 @@ export function isWorkflowDoCall(
17
18
 
18
19
  const propAccess = node.expression
19
20
  return (
20
- propAccess.name.text === 'do' &&
21
+ (propAccess.name.text === 'do' ||
22
+ propAccess.name.text === 'expectEventually') &&
21
23
  ts.isIdentifier(propAccess.expression) &&
22
24
  propAccess.expression.text === 'workflow'
23
25
  )
24
26
  }
25
27
 
28
+ /**
29
+ * Check if a call expression is workflow.expectEventually()
30
+ */
31
+ export function isWorkflowExpectEventuallyCall(
32
+ node: ts.CallExpression
33
+ ): boolean {
34
+ return (
35
+ ts.isPropertyAccessExpression(node.expression) &&
36
+ node.expression.name.text === 'expectEventually' &&
37
+ ts.isIdentifier(node.expression.expression) &&
38
+ node.expression.expression.text === 'workflow'
39
+ )
40
+ }
41
+
42
+ /**
43
+ * Extract the actor NAME from a step options object literal:
44
+ * `{ actor: actors.x }` → 'x' (also `actors['x']`). Undefined when absent
45
+ * or not statically resolvable.
46
+ */
47
+ export function extractActorFromOptions(
48
+ optionsArg: ts.Expression | undefined
49
+ ): string | undefined {
50
+ if (!optionsArg || !ts.isObjectLiteralExpression(optionsArg)) return
51
+ for (const prop of optionsArg.properties) {
52
+ if (
53
+ ts.isPropertyAssignment(prop) &&
54
+ ts.isIdentifier(prop.name) &&
55
+ prop.name.text === 'actor'
56
+ ) {
57
+ const init = prop.initializer
58
+ if (ts.isPropertyAccessExpression(init)) {
59
+ return init.name.text
60
+ }
61
+ if (
62
+ ts.isElementAccessExpression(init) &&
63
+ ts.isStringLiteral(init.argumentExpression)
64
+ ) {
65
+ return init.argumentExpression.text
66
+ }
67
+ }
68
+ }
69
+ return
70
+ }
71
+
26
72
  /**
27
73
  * Check if a call expression is workflow.sleep()
28
74
  */
@@ -93,6 +93,12 @@ function convertStepToNode(
93
93
  rpcName: step.rpcName,
94
94
  next: nextNodeId,
95
95
  }
96
+ if (step.actor) {
97
+ node.actor = step.actor
98
+ }
99
+ if (step.expectEventually) {
100
+ node.expectEventually = true
101
+ }
96
102
  if (step.inputs) {
97
103
  if (step.inputs === 'passthrough') {
98
104
  // Entire data is passed through - store as reference to trigger
@@ -403,8 +409,10 @@ export function convertDslToGraph(
403
409
  name: workflowName,
404
410
  pikkuFuncId: meta.pikkuFuncId,
405
411
  source,
412
+ title: meta.title,
406
413
  description: meta.description,
407
414
  tags: meta.tags,
415
+ actors: meta.actors,
408
416
  context: meta.context,
409
417
  nodes: nodesRecord,
410
418
  entryNodeIds,
@@ -142,6 +142,10 @@ export interface FunctionNode extends BaseNode {
142
142
  outputVar?: string
143
143
  /** Hash of nodeId + RPC input/output schemas for version detection */
144
144
  stepHash?: string
145
+ /** User-flow actor name this step runs as (workflow.do {actor: actors.x}) */
146
+ actor?: string
147
+ /** True for workflow.expectEventually polling steps (user flows) */
148
+ expectEventually?: boolean
145
149
  }
146
150
 
147
151
  /**
@@ -191,10 +195,14 @@ export interface SerializedWorkflowGraph {
191
195
  pikkuFuncId: string
192
196
  /** Source type: 'dsl' for pikkuWorkflowFunc, 'graph' for pikkuWorkflowGraph */
193
197
  source: WorkflowSourceType
198
+ /** Optional short display name */
199
+ title?: string
194
200
  /** Optional description */
195
201
  description?: string
196
202
  /** Tags for organization */
197
203
  tags?: string[]
204
+ /** Actor names a user flow's steps run as */
205
+ actors?: string[]
198
206
  /** If true, workflow always executes inline without queues */
199
207
  inline?: boolean
200
208
  /** Workflow context/state variables (from Zod schema) */