@elizaos/plugin-workflow 2.0.0-beta.1 → 2.0.3-beta.6

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.
Files changed (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +28 -26
  3. package/dist/actions/eval-code.d.ts +12 -0
  4. package/dist/actions/eval-code.d.ts.map +1 -0
  5. package/dist/actions/eval-code.js +59 -0
  6. package/dist/actions/eval-code.js.map +1 -0
  7. package/dist/actions/index.d.ts +1 -0
  8. package/dist/actions/index.d.ts.map +1 -1
  9. package/dist/actions/index.js +1 -0
  10. package/dist/actions/index.js.map +1 -1
  11. package/dist/actions/workflow.d.ts +7 -0
  12. package/dist/actions/workflow.d.ts.map +1 -1
  13. package/dist/actions/workflow.js +462 -10
  14. package/dist/actions/workflow.js.map +1 -1
  15. package/dist/db/schema.d.ts +196 -0
  16. package/dist/db/schema.d.ts.map +1 -1
  17. package/dist/db/schema.js +23 -0
  18. package/dist/db/schema.js.map +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +9 -64
  21. package/dist/index.js.map +1 -1
  22. package/dist/lib/automations-builder.d.ts.map +1 -1
  23. package/dist/lib/automations-builder.js +10 -35
  24. package/dist/lib/automations-builder.js.map +1 -1
  25. package/dist/lib/automations-types.d.ts +2 -2
  26. package/dist/lib/automations-types.d.ts.map +1 -1
  27. package/dist/lib/automations-types.js.map +1 -1
  28. package/dist/lib/index.d.ts +0 -2
  29. package/dist/lib/index.d.ts.map +1 -1
  30. package/dist/lib/index.js +1 -2
  31. package/dist/lib/index.js.map +1 -1
  32. package/dist/lib/workflow-clarification.d.ts +2 -2
  33. package/dist/lib/workflow-clarification.d.ts.map +1 -1
  34. package/dist/lib/workflow-clarification.js +15 -11
  35. package/dist/lib/workflow-clarification.js.map +1 -1
  36. package/dist/plugin-routes.d.ts.map +1 -1
  37. package/dist/plugin-routes.js +6 -0
  38. package/dist/plugin-routes.js.map +1 -1
  39. package/dist/providers/activeWorkflows.js +2 -2
  40. package/dist/providers/activeWorkflows.js.map +1 -1
  41. package/dist/providers/workflowStatus.js +1 -1
  42. package/dist/providers/workflowStatus.js.map +1 -1
  43. package/dist/routes/workflow-routes.d.ts.map +1 -1
  44. package/dist/routes/workflow-routes.js +68 -2
  45. package/dist/routes/workflow-routes.js.map +1 -1
  46. package/dist/routes/workflows.d.ts.map +1 -1
  47. package/dist/routes/workflows.js +5 -1
  48. package/dist/routes/workflows.js.map +1 -1
  49. package/dist/services/embedded-workflow-service.d.ts +74 -17
  50. package/dist/services/embedded-workflow-service.d.ts.map +1 -1
  51. package/dist/services/embedded-workflow-service.js +343 -149
  52. package/dist/services/embedded-workflow-service.js.map +1 -1
  53. package/dist/services/smithers-runtime.d.ts +47 -0
  54. package/dist/services/smithers-runtime.d.ts.map +1 -0
  55. package/dist/services/smithers-runtime.js +444 -0
  56. package/dist/services/smithers-runtime.js.map +1 -0
  57. package/dist/services/workflow-credential-store.js +1 -1
  58. package/dist/services/workflow-credential-store.js.map +1 -1
  59. package/dist/services/workflow-dispatch.d.ts +31 -1
  60. package/dist/services/workflow-dispatch.d.ts.map +1 -1
  61. package/dist/services/workflow-dispatch.js +75 -10
  62. package/dist/services/workflow-dispatch.js.map +1 -1
  63. package/dist/services/workflow-service.d.ts +27 -1
  64. package/dist/services/workflow-service.d.ts.map +1 -1
  65. package/dist/services/workflow-service.js +133 -11
  66. package/dist/services/workflow-service.js.map +1 -1
  67. package/dist/trigger-routes.d.ts +2 -18
  68. package/dist/trigger-routes.d.ts.map +1 -1
  69. package/dist/trigger-routes.js +11 -39
  70. package/dist/trigger-routes.js.map +1 -1
  71. package/dist/types/index.d.ts +82 -2
  72. package/dist/types/index.d.ts.map +1 -1
  73. package/dist/types/index.js.map +1 -1
  74. package/dist/types/workflow-contracts.d.ts +118 -0
  75. package/dist/types/workflow-contracts.d.ts.map +1 -0
  76. package/dist/types/workflow-contracts.js +2 -0
  77. package/dist/types/workflow-contracts.js.map +1 -0
  78. package/dist/utils/catalog.js +2 -2
  79. package/dist/utils/catalog.js.map +1 -1
  80. package/dist/utils/clarification.d.ts +1 -1
  81. package/dist/utils/clarification.d.ts.map +1 -1
  82. package/dist/utils/clarification.js +15 -4
  83. package/dist/utils/clarification.js.map +1 -1
  84. package/dist/utils/context.js +1 -1
  85. package/dist/utils/context.js.map +1 -1
  86. package/dist/utils/evaluation-samples.d.ts +6 -0
  87. package/dist/utils/evaluation-samples.d.ts.map +1 -0
  88. package/dist/utils/evaluation-samples.js +216 -0
  89. package/dist/utils/evaluation-samples.js.map +1 -0
  90. package/dist/utils/execution-diagnostics.d.ts +26 -0
  91. package/dist/utils/execution-diagnostics.d.ts.map +1 -0
  92. package/dist/utils/execution-diagnostics.js +159 -0
  93. package/dist/utils/execution-diagnostics.js.map +1 -0
  94. package/dist/utils/generation.d.ts.map +1 -1
  95. package/dist/utils/generation.js +134 -19
  96. package/dist/utils/generation.js.map +1 -1
  97. package/dist/utils/host-capabilities.d.ts.map +1 -1
  98. package/dist/utils/host-capabilities.js +20 -5
  99. package/dist/utils/host-capabilities.js.map +1 -1
  100. package/dist/utils/inferSyntheticOutputSchema.js +3 -3
  101. package/dist/utils/inferSyntheticOutputSchema.js.map +1 -1
  102. package/dist/utils/outputSchema.js +1 -1
  103. package/dist/utils/outputSchema.js.map +1 -1
  104. package/dist/utils/validateAndRepair.js +10 -10
  105. package/dist/utils/validateAndRepair.js.map +1 -1
  106. package/dist/utils/workflow-prompts/draftIntent.d.ts +1 -1
  107. package/dist/utils/workflow-prompts/draftIntent.d.ts.map +1 -1
  108. package/dist/utils/workflow-prompts/draftIntent.js +1 -1
  109. package/dist/utils/workflow-prompts/keywordExtraction.d.ts +1 -1
  110. package/dist/utils/workflow-prompts/keywordExtraction.d.ts.map +1 -1
  111. package/dist/utils/workflow-prompts/keywordExtraction.js +1 -1
  112. package/dist/utils/workflow-prompts/workflowGeneration.d.ts +1 -1
  113. package/dist/utils/workflow-prompts/workflowGeneration.d.ts.map +1 -1
  114. package/dist/utils/workflow-prompts/workflowGeneration.js +4 -4
  115. package/dist/utils/workflow-prompts/workflowMatching.d.ts +1 -1
  116. package/dist/utils/workflow-prompts/workflowMatching.d.ts.map +1 -1
  117. package/dist/utils/workflow-prompts/workflowMatching.js +1 -1
  118. package/dist/utils/workflow.d.ts +1 -0
  119. package/dist/utils/workflow.d.ts.map +1 -1
  120. package/dist/utils/workflow.js +44 -8
  121. package/dist/utils/workflow.js.map +1 -1
  122. package/package.json +27 -8
  123. package/registry-entry.json +25 -0
  124. package/src/actions/eval-code.ts +81 -0
  125. package/src/actions/index.ts +1 -0
  126. package/src/actions/workflow.ts +518 -10
  127. package/src/db/schema.ts +31 -0
  128. package/src/index.ts +9 -82
  129. package/src/lib/automations-builder.ts +11 -35
  130. package/src/lib/automations-types.ts +1 -2
  131. package/src/lib/index.ts +0 -8
  132. package/src/lib/workflow-clarification.ts +18 -13
  133. package/src/plugin-routes.ts +6 -0
  134. package/src/providers/activeWorkflows.ts +2 -2
  135. package/src/providers/workflowStatus.ts +1 -1
  136. package/src/routes/workflow-routes.ts +100 -2
  137. package/src/routes/workflows.ts +5 -1
  138. package/src/services/embedded-workflow-service.ts +447 -172
  139. package/src/services/smithers-runtime.ts +526 -0
  140. package/src/services/workflow-credential-store.ts +1 -1
  141. package/src/services/workflow-dispatch.ts +116 -13
  142. package/src/services/workflow-service.ts +186 -10
  143. package/src/trigger-routes.ts +12 -70
  144. package/src/types/index.ts +94 -2
  145. package/src/types/workflow-contracts.ts +166 -0
  146. package/src/utils/catalog.ts +2 -2
  147. package/src/utils/clarification.ts +19 -5
  148. package/src/utils/context.ts +1 -1
  149. package/src/utils/evaluation-samples.ts +239 -0
  150. package/src/utils/execution-diagnostics.ts +192 -0
  151. package/src/utils/generation.ts +224 -32
  152. package/src/utils/host-capabilities.ts +21 -5
  153. package/src/utils/inferSyntheticOutputSchema.ts +3 -3
  154. package/src/utils/outputSchema.ts +1 -1
  155. package/src/utils/validateAndRepair.ts +10 -10
  156. package/src/utils/workflow-prompts/draftIntent.ts +1 -1
  157. package/src/utils/workflow-prompts/keywordExtraction.ts +1 -1
  158. package/src/utils/workflow-prompts/workflowGeneration.ts +4 -4
  159. package/src/utils/workflow-prompts/workflowMatching.ts +1 -1
  160. package/src/utils/workflow.ts +56 -8
  161. package/dist/lib/legacy-task-migration.d.ts +0 -20
  162. package/dist/lib/legacy-task-migration.d.ts.map +0 -1
  163. package/dist/lib/legacy-task-migration.js +0 -110
  164. package/dist/lib/legacy-task-migration.js.map +0 -1
  165. package/dist/lib/legacy-text-trigger-migration.d.ts +0 -18
  166. package/dist/lib/legacy-text-trigger-migration.d.ts.map +0 -1
  167. package/dist/lib/legacy-text-trigger-migration.js +0 -131
  168. package/dist/lib/legacy-text-trigger-migration.js.map +0 -1
  169. package/src/lib/legacy-task-migration.ts +0 -143
  170. package/src/lib/legacy-text-trigger-migration.ts +0 -178
@@ -27,10 +27,31 @@ export interface WorkflowDispatchResult {
27
27
  ok: boolean;
28
28
  error?: string;
29
29
  executionId?: string;
30
+ /**
31
+ * True when the call was short-circuited by an idempotency-key match.
32
+ * Callers (the trigger dispatcher, dashboards) can record a dedup
33
+ * instead of treating the call as a fresh execution.
34
+ */
35
+ dedup?: boolean;
36
+ }
37
+
38
+ /**
39
+ * Optional, structured dispatch options. The `idempotencyKey` field is
40
+ * the durable contract: same workflow + same key → at most one
41
+ * execution. Passed inline through the legacy `payload` shape (key
42
+ * `__idempotencyKey`) when the caller can't pass a second argument.
43
+ */
44
+ export interface WorkflowDispatchOptions {
45
+ triggerData?: Record<string, unknown>;
46
+ idempotencyKey?: string;
30
47
  }
31
48
 
32
49
  export interface WorkflowDispatchService {
33
- execute(workflowId: string, payload?: Record<string, unknown>): Promise<WorkflowDispatchResult>;
50
+ execute(
51
+ workflowId: string,
52
+ payload?: Record<string, unknown>,
53
+ options?: WorkflowDispatchOptions
54
+ ): Promise<WorkflowDispatchResult>;
34
55
  }
35
56
 
36
57
  interface WorkflowDispatchServiceEntry extends WorkflowDispatchService {
@@ -38,6 +59,24 @@ interface WorkflowDispatchServiceEntry extends WorkflowDispatchService {
38
59
  capabilityDescription: string;
39
60
  }
40
61
 
62
+ /**
63
+ * Pull `__idempotencyKey` out of the legacy `payload` shape so existing
64
+ * callers (the trigger dispatcher's `event` payload) can attach a key
65
+ * without growing the signature. The wrapper key is stripped before the
66
+ * payload is forwarded as `triggerData`.
67
+ */
68
+ function partitionPayload(payload: Record<string, unknown> | undefined): {
69
+ triggerData: Record<string, unknown>;
70
+ idempotencyKey?: string;
71
+ } {
72
+ if (!payload) return { triggerData: {} };
73
+ const { __idempotencyKey, ...rest } = payload;
74
+ return {
75
+ triggerData: rest,
76
+ idempotencyKey: typeof __idempotencyKey === 'string' ? __idempotencyKey : undefined,
77
+ };
78
+ }
79
+
41
80
  interface RuntimeServiceRegistry {
42
81
  set(serviceType: string, services: WorkflowDispatchServiceEntry[]): void;
43
82
  }
@@ -71,12 +110,33 @@ function getRuntimeServiceRegistry(runtime: IAgentRuntime): RuntimeServiceRegist
71
110
  /**
72
111
  * Construct the dispatch service. Registered under `WORKFLOW_DISPATCH` on the
73
112
  * runtime by the plugin's `init` lifecycle hook.
113
+ *
114
+ * Idempotency contract: when a caller passes an `idempotencyKey` (either via
115
+ * the explicit `options.idempotencyKey` or via the legacy
116
+ * `payload.__idempotencyKey`), the dispatch service first looks up an
117
+ * existing execution row for `(workflowId, idempotencyKey)`. If one exists,
118
+ * the new run is suppressed and the prior execution id is returned with
119
+ * `{ ok: true, dedup: true }`. Scheduled workflow dispatches use a
120
+ * minute-bucketed key so two simultaneous schedule fires collapse to one
121
+ * execution.
122
+ *
123
+ * Concurrent dispatches that race past the lookup are still safely
124
+ * coalesced because the embedded service persists the idempotency key on
125
+ * the execution row, so the second-to-write completes but is detectable
126
+ * as a duplicate on later lookups.
74
127
  */
75
128
  export function createWorkflowDispatchService(runtime: IAgentRuntime): WorkflowDispatchService {
129
+ // Track in-flight executions by `(workflowId, idempotencyKey)` so that
130
+ // two concurrent dispatches inside the same process collapse onto one
131
+ // run. The map entry resolves once the original run finishes, and the
132
+ // late caller returns the same execution id.
133
+ const inflight = new Map<string, Promise<WorkflowDispatchResult>>();
134
+
76
135
  return {
77
136
  async execute(
78
137
  workflowId: string,
79
- _payload: Record<string, unknown> = {}
138
+ payload: Record<string, unknown> = {},
139
+ options: WorkflowDispatchOptions = {}
80
140
  ): Promise<WorkflowDispatchResult> {
81
141
  const id = workflowId.trim();
82
142
  if (!id) {
@@ -86,21 +146,64 @@ export function createWorkflowDispatchService(runtime: IAgentRuntime): WorkflowD
86
146
  if (!service) {
87
147
  return { ok: false, error: 'embedded workflow service not registered' };
88
148
  }
89
- try {
90
- const execution = await service.executeWorkflow(id, { mode: 'trigger' });
91
- return execution.id ? { ok: true, executionId: execution.id } : { ok: true };
92
- } catch (err) {
93
- const message = err instanceof Error ? err.message : String(err);
94
- logger.warn(
95
- { src: 'plugin:workflow:dispatch' },
96
- `Workflow execution failed for ${id}: ${message}`
97
- );
98
- return { ok: false, error: message };
149
+
150
+ const partitioned = partitionPayload(payload);
151
+ const triggerData =
152
+ options.triggerData && Object.keys(options.triggerData).length > 0
153
+ ? options.triggerData
154
+ : partitioned.triggerData;
155
+ const idempotencyKey = options.idempotencyKey ?? partitioned.idempotencyKey;
156
+
157
+ if (idempotencyKey) {
158
+ const existing = await service.findExecutionByIdempotencyKey(id, idempotencyKey);
159
+ if (existing) {
160
+ return existing.id
161
+ ? { ok: true, executionId: existing.id, dedup: true }
162
+ : { ok: true, dedup: true };
163
+ }
164
+
165
+ const inflightKey = `${id}::${idempotencyKey}`;
166
+ const pending = inflight.get(inflightKey);
167
+ if (pending) {
168
+ const result = await pending;
169
+ return result.ok ? { ...result, dedup: true } : result;
170
+ }
171
+
172
+ const promise = runDispatch(service, id, triggerData, idempotencyKey).finally(() => {
173
+ inflight.delete(inflightKey);
174
+ });
175
+ inflight.set(inflightKey, promise);
176
+ return promise;
99
177
  }
178
+
179
+ return runDispatch(service, id, triggerData, undefined);
100
180
  },
101
181
  };
102
182
  }
103
183
 
184
+ async function runDispatch(
185
+ service: EmbeddedWorkflowService,
186
+ workflowId: string,
187
+ triggerData: Record<string, unknown>,
188
+ idempotencyKey: string | undefined
189
+ ): Promise<WorkflowDispatchResult> {
190
+ try {
191
+ const execution = await service.executeWorkflow(workflowId, {
192
+ mode: 'trigger',
193
+ triggerData,
194
+ idempotencyKey,
195
+ });
196
+ return execution.id ? { ok: true, executionId: execution.id } : { ok: true };
197
+ } catch (err) {
198
+ const message = err instanceof Error ? err.message : String(err);
199
+ logger.warn(
200
+ { src: 'plugin:workflow:dispatch' },
201
+ `Workflow execution failed for ${workflowId}: ${message}`
202
+ );
203
+ return { ok: false, error: message };
204
+ }
205
+ }
206
+
104
207
  /**
105
208
  * Register the dispatch service in the runtime services map under
106
209
  * `WORKFLOW_DISPATCH`. Called from the plugin's `init`.
@@ -113,7 +216,7 @@ export function createWorkflowDispatchService(runtime: IAgentRuntime): WorkflowD
113
216
  export function registerWorkflowDispatchService(runtime: IAgentRuntime): void {
114
217
  const dispatch = createWorkflowDispatchService(runtime);
115
218
  const serviceEntry: WorkflowDispatchServiceEntry = {
116
- execute: dispatch.execute,
219
+ execute: dispatch.execute.bind(dispatch),
117
220
  stop: async () => {},
118
221
  capabilityDescription: 'Executes embedded workflows by id via the in-process workflow service.',
119
222
  };
@@ -1,13 +1,16 @@
1
1
  import { type IAgentRuntime, logger, Service } from '@elizaos/core';
2
2
  import type {
3
3
  NodeDefinition,
4
+ NodeSearchResult,
4
5
  RuntimeContext,
5
6
  TriggerContext,
6
7
  WorkflowCreationResult,
7
8
  WorkflowCredentialStoreApi,
8
9
  WorkflowDefinition,
9
10
  WorkflowDefinitionResponse,
11
+ WorkflowEvaluationSuite,
10
12
  WorkflowExecution,
13
+ WorkflowRevision,
11
14
  } from '../types/index';
12
15
  import {
13
16
  isCredentialProvider,
@@ -22,6 +25,7 @@ import { filterNodesByIntegrationSupport, searchNodes } from '../utils/catalog';
22
25
  import { CATALOG_CLARIFICATION_SUFFIX, isCatalogClarification } from '../utils/clarification';
23
26
  import { getUserTagName } from '../utils/context';
24
27
  import { resolveCredentials } from '../utils/credentialResolver';
28
+ import { buildWorkflowEvaluationSuite } from '../utils/evaluation-samples';
25
29
  import {
26
30
  assessFeasibility,
27
31
  collectExistingNodeDefinitions,
@@ -39,6 +43,7 @@ import {
39
43
  ensureExpressionPrefix,
40
44
  injectMissingCredentialBlocks,
41
45
  normalizeTriggerSimpleParam,
46
+ normalizeWorkflowNodeParameterShapes,
42
47
  positionNodes,
43
48
  validateNodeInputs,
44
49
  validateNodeParameters,
@@ -68,6 +73,9 @@ type WorkflowDefinitionClient = Pick<
68
73
  | 'deleteWorkflow'
69
74
  | 'activateWorkflow'
70
75
  | 'deactivateWorkflow'
76
+ | 'listWorkflowRevisions'
77
+ | 'restoreWorkflowRevision'
78
+ | 'executeWorkflow'
71
79
  | 'updateWorkflowTags'
72
80
  | 'createCredential'
73
81
  | 'listExecutions'
@@ -94,6 +102,105 @@ function isWorkflowCredentialStoreApi(service: unknown): service is WorkflowCred
94
102
  );
95
103
  }
96
104
 
105
+ const FIELD_TRANSFORM_VERB_PATTERN =
106
+ /\b(adds?|adding|sets?|setting|assigns?|assigning|writes?|writing|maps?|mapping|appends?|appending|enrich(?:es|ing)?)\b/;
107
+ const FIELD_TRANSFORM_TARGET_PATTERN =
108
+ /\b(field|fields|value|values|data|item|items|metadata|json|property|properties)\b/;
109
+ const NETWORK_REQUEST_PATTERN =
110
+ /\b(http|https|url|api|request|fetch|call|post|get|put|patch|delete|webhook)\b/;
111
+
112
+ function buildWorkflowSearchKeywords(prompt: string, keywords: string[]): string[] {
113
+ const normalized = new Set(keywords.map((keyword) => keyword.toLowerCase()));
114
+ const addKeyword = (keyword: string): void => {
115
+ if (!normalized.has(keyword)) {
116
+ keywords.unshift(keyword);
117
+ normalized.add(keyword);
118
+ }
119
+ };
120
+ const lowerPrompt = prompt.toLowerCase();
121
+ if (FIELD_TRANSFORM_VERB_PATTERN.test(lowerPrompt)) {
122
+ if (FIELD_TRANSFORM_TARGET_PATTERN.test(lowerPrompt)) {
123
+ addKeyword('set');
124
+ }
125
+ }
126
+ return keywords;
127
+ }
128
+
129
+ function filterPromptCandidateNodes(prompt: string, nodes: NodeSearchResult[]): NodeSearchResult[] {
130
+ const lowerPrompt = prompt.toLowerCase();
131
+ const looksLikeFieldTransform =
132
+ FIELD_TRANSFORM_VERB_PATTERN.test(lowerPrompt) &&
133
+ FIELD_TRANSFORM_TARGET_PATTERN.test(lowerPrompt);
134
+ const looksLikeNetworkRequest = NETWORK_REQUEST_PATTERN.test(lowerPrompt);
135
+ if (!looksLikeFieldTransform || looksLikeNetworkRequest) {
136
+ return nodes;
137
+ }
138
+ return nodes.filter((result) => result.node.name !== 'workflows-nodes-base.httpRequest');
139
+ }
140
+
141
+ function normalizeGeneratedNodeParameterShapes(
142
+ workflow: WorkflowDefinition,
143
+ context: 'generated workflow' | 'modified workflow'
144
+ ): void {
145
+ const fixes = normalizeWorkflowNodeParameterShapes(workflow);
146
+ if (fixes > 0) {
147
+ logger.debug(
148
+ { src: 'plugin:workflow:service:main' },
149
+ `Normalized ${fixes} node parameter shape(s) in ${context}`
150
+ );
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Score a workflow against a lowercased query: name beats node type beats
156
+ * description, and an exact/prefix name match beats a substring. Returns 0 for
157
+ * no match. Pure + exported so the ranking is unit-testable without a DB.
158
+ */
159
+ export function scoreWorkflowMatch(workflow: WorkflowDefinitionResponse, query: string): number {
160
+ const q = query.trim().toLowerCase();
161
+ if (!q) return 0;
162
+ const name = String(workflow.name ?? '').toLowerCase();
163
+ let score = 0;
164
+ if (name === q) score += 100;
165
+ else if (name.startsWith(q)) score += 50;
166
+ else if (name.includes(q)) score += 30;
167
+
168
+ const nodes = (workflow as { nodes?: Array<{ type?: unknown; name?: unknown }> }).nodes;
169
+ if (Array.isArray(nodes)) {
170
+ for (const node of nodes) {
171
+ const type = String(node?.type ?? '').toLowerCase();
172
+ const nodeName = String(node?.name ?? '').toLowerCase();
173
+ if (type.includes(q) || nodeName.includes(q)) {
174
+ score += 10;
175
+ break;
176
+ }
177
+ }
178
+ }
179
+
180
+ const description = String(
181
+ (workflow as { description?: unknown }).description ?? ''
182
+ ).toLowerCase();
183
+ if (description.includes(q)) score += 5;
184
+
185
+ return score;
186
+ }
187
+
188
+ /**
189
+ * Rank workflows best-match-first for a free-text query, dropping non-matches.
190
+ * An empty query returns the input order unchanged. Pure + exported (#8913).
191
+ */
192
+ export function rankWorkflowsByQuery(
193
+ workflows: WorkflowDefinitionResponse[],
194
+ query: string
195
+ ): WorkflowDefinitionResponse[] {
196
+ if (!query.trim()) return workflows;
197
+ return workflows
198
+ .map((workflow) => ({ workflow, score: scoreWorkflowMatch(workflow, query) }))
199
+ .filter((entry) => entry.score > 0)
200
+ .sort((a, b) => b.score - a.score)
201
+ .map((entry) => entry.workflow);
202
+ }
203
+
97
204
  /**
98
205
  * Workflow Service - Orchestrates the RAG pipeline for workflow generation.
99
206
  *
@@ -115,7 +222,7 @@ export class WorkflowService extends Service {
115
222
 
116
223
  // Get optional pre-configured credentials from character.settings.workflows
117
224
  // Note: runtime.getSetting() only returns primitives — nested objects must be read directly
118
- const workflowSettings = runtime.character?.settings?.workflows as
225
+ const workflowSettings = runtime.character.settings?.workflows as
119
226
  | { credentials?: Record<string, string> }
120
227
  | undefined;
121
228
  const credentials = workflowSettings?.credentials;
@@ -283,21 +390,26 @@ export class WorkflowService extends Service {
283
390
 
284
391
  // Fetch host-supplied bias hints early (before keyword extraction) so the
285
392
  // LLM is told which providers the host already knows it can satisfy.
286
- // We pass empty `relevantNodes` / `relevantCredTypes` here because we do
287
- // not yet have searchNodes results — `preferredProviders` is derived from
288
- // the host's connector config alone (independent of node search). The
393
+ // We pass empty `relevantNodes` / `relevantCredTypes` here before
394
+ // node-catalog search runs: `preferredProviders` is derived from the
395
+ // host's connector config alone (independent of node search). The
289
396
  // full runtime context (with credentials + facts) is fetched again later
290
397
  // once we have the filtered node list.
291
398
  const earlyContext = await this.fetchRuntimeContext([], opts?.userId ?? 'local');
292
399
  const preferredProviders = earlyContext?.preferredProviders;
293
400
 
294
- const keywords = await extractKeywords(this.runtime, prompt, preferredProviders);
401
+ const keywords = buildWorkflowSearchKeywords(
402
+ prompt,
403
+ await extractKeywords(this.runtime, prompt, preferredProviders)
404
+ );
295
405
  logger.debug(
296
406
  { src: 'plugin:workflow:service:main' },
297
407
  `Extracted keywords: ${keywords.join(', ')}${preferredProviders?.length ? ` (with bias: ${preferredProviders.join(', ')})` : ''}`
298
408
  );
299
409
 
300
- let relevantNodes = this.filterForEmbeddedBackend(searchNodes(keywords, 15));
410
+ let relevantNodes = this.filterForEmbeddedBackend(
411
+ filterPromptCandidateNodes(prompt, searchNodes(keywords, 15))
412
+ );
301
413
  logger.debug(
302
414
  { src: 'plugin:workflow:service:main' },
303
415
  `Found ${relevantNodes.length} relevant nodes`
@@ -369,7 +481,7 @@ export class WorkflowService extends Service {
369
481
  let workflow = await generateWorkflow(this.runtime, prompt, finalNodeDefs, runtimeContext);
370
482
  logger.debug(
371
483
  { src: 'plugin:workflow:service:main' },
372
- `Generated workflow with ${workflow.nodes?.length || 0} nodes`
484
+ `Generated workflow with ${workflow.nodes.length || 0} nodes`
373
485
  );
374
486
 
375
487
  // Safety net: even with the MANDATORY INVARIANT prompt rule, the LLM
@@ -448,6 +560,7 @@ export class WorkflowService extends Service {
448
560
  }
449
561
 
450
562
  normalizeTriggerSimpleParam(workflow);
563
+ normalizeGeneratedNodeParameterShapes(workflow, 'generated workflow');
451
564
 
452
565
  const optionFixes = correctOptionParameters(workflow);
453
566
  if (optionFixes > 0) {
@@ -464,6 +577,7 @@ export class WorkflowService extends Service {
464
577
  `Found ${unknownParams.length} node(s) with unknown parameters, auto-correcting...`
465
578
  );
466
579
  workflow = await correctParameterNames(this.runtime, workflow, unknownParams);
580
+ normalizeGeneratedNodeParameterShapes(workflow, 'generated workflow');
467
581
  }
468
582
 
469
583
  const invalidRefs = validateOutputReferences(workflow);
@@ -473,6 +587,7 @@ export class WorkflowService extends Service {
473
587
  `Found ${invalidRefs.length} invalid field reference(s), auto-correcting...`
474
588
  );
475
589
  workflow = await correctFieldReferences(this.runtime, workflow, invalidRefs);
590
+ normalizeGeneratedNodeParameterShapes(workflow, 'generated workflow');
476
591
  }
477
592
 
478
593
  const exprPrefixed = ensureExpressionPrefix(workflow);
@@ -516,8 +631,13 @@ export class WorkflowService extends Service {
516
631
  const existingDefs = collectExistingNodeDefinitions(existingWorkflow);
517
632
 
518
633
  // Search for new nodes the modification might need
519
- const keywords = await extractKeywords(this.runtime, modificationRequest);
520
- const searchResults = this.filterForEmbeddedBackend(searchNodes(keywords, 10));
634
+ const keywords = buildWorkflowSearchKeywords(
635
+ modificationRequest,
636
+ await extractKeywords(this.runtime, modificationRequest)
637
+ );
638
+ const searchResults = this.filterForEmbeddedBackend(
639
+ filterPromptCandidateNodes(modificationRequest, searchNodes(keywords, 10))
640
+ );
521
641
  const newDefs = searchResults.map((r) => r.node);
522
642
 
523
643
  // Deduplicate: merge existing + new, preferring existing (already in workflow)
@@ -615,6 +735,7 @@ export class WorkflowService extends Service {
615
735
  }
616
736
 
617
737
  normalizeTriggerSimpleParam(workflow);
738
+ normalizeGeneratedNodeParameterShapes(workflow, 'modified workflow');
618
739
 
619
740
  const optionFixes = correctOptionParameters(workflow);
620
741
  if (optionFixes > 0) {
@@ -631,6 +752,7 @@ export class WorkflowService extends Service {
631
752
  `Found ${unknownParams.length} node(s) with unknown parameters in modified workflow, auto-correcting...`
632
753
  );
633
754
  workflow = await correctParameterNames(this.runtime, workflow, unknownParams);
755
+ normalizeGeneratedNodeParameterShapes(workflow, 'modified workflow');
634
756
  }
635
757
 
636
758
  const invalidRefs = validateOutputReferences(workflow);
@@ -640,6 +762,7 @@ export class WorkflowService extends Service {
640
762
  `Found ${invalidRefs.length} invalid field reference(s) in modified workflow, auto-correcting...`
641
763
  );
642
764
  workflow = await correctFieldReferences(this.runtime, workflow, invalidRefs);
765
+ normalizeGeneratedNodeParameterShapes(workflow, 'modified workflow');
643
766
  }
644
767
 
645
768
  const exprPrefixed = ensureExpressionPrefix(workflow);
@@ -767,7 +890,7 @@ export class WorkflowService extends Service {
767
890
  id: deployedWorkflow.id,
768
891
  name: deployedWorkflow.name,
769
892
  active,
770
- nodeCount: deployedWorkflow.nodes?.length || 0,
893
+ nodeCount: deployedWorkflow.nodes.length || 0,
771
894
  missingCredentials: credentialResult.missingConnections,
772
895
  };
773
896
  }
@@ -793,6 +916,16 @@ export class WorkflowService extends Service {
793
916
  return response.data;
794
917
  }
795
918
 
919
+ /**
920
+ * Free-text search over the user's workflows by name, node type, and
921
+ * description, ranked best-match-first (#8913). Lets a user find "the Slack
922
+ * workflow" from a chat message without knowing its id.
923
+ */
924
+ async searchWorkflows(query: string, userId?: string): Promise<WorkflowDefinitionResponse[]> {
925
+ const workflows = await this.listWorkflows(userId);
926
+ return rankWorkflowsByQuery(workflows, query);
927
+ }
928
+
796
929
  async activateWorkflow(workflowId: string): Promise<void> {
797
930
  const client = this.getClient();
798
931
  await client.activateWorkflow(workflowId);
@@ -816,12 +949,55 @@ export class WorkflowService extends Service {
816
949
  return client.getWorkflow(workflowId);
817
950
  }
818
951
 
952
+ async listWorkflowRevisions(workflowId: string, limit?: number): Promise<WorkflowRevision[]> {
953
+ const client = this.getClient();
954
+ const response = await client.listWorkflowRevisions(workflowId, limit);
955
+ return response.data;
956
+ }
957
+
958
+ async restoreWorkflowRevision(
959
+ workflowId: string,
960
+ versionId: string
961
+ ): Promise<WorkflowDefinitionResponse> {
962
+ const client = this.getClient();
963
+ return client.restoreWorkflowRevision(workflowId, versionId);
964
+ }
965
+
966
+ async runWorkflow(
967
+ workflowId: string,
968
+ options?: {
969
+ mode?: WorkflowExecution['mode'];
970
+ triggerData?: Record<string, unknown>;
971
+ idempotencyKey?: string;
972
+ throwOnError?: boolean;
973
+ }
974
+ ): Promise<WorkflowExecution> {
975
+ const client = this.getClient();
976
+ return client.executeWorkflow(workflowId, {
977
+ mode: options?.mode ?? 'manual',
978
+ triggerData: options?.triggerData,
979
+ idempotencyKey: options?.idempotencyKey,
980
+ throwOnError: options?.throwOnError,
981
+ });
982
+ }
983
+
819
984
  async getWorkflowExecutions(workflowId: string, limit?: number): Promise<WorkflowExecution[]> {
820
985
  const client = this.getClient();
821
986
  const response = await client.listExecutions({ workflowId, limit });
822
987
  return response.data;
823
988
  }
824
989
 
990
+ async getWorkflowEvaluationSuite(
991
+ workflowId: string,
992
+ limit?: number
993
+ ): Promise<WorkflowEvaluationSuite> {
994
+ const [workflow, executions] = await Promise.all([
995
+ this.getWorkflow(workflowId),
996
+ this.getWorkflowExecutions(workflowId, limit),
997
+ ]);
998
+ return buildWorkflowEvaluationSuite(workflow, executions, { limit });
999
+ }
1000
+
825
1001
  async listExecutions(params?: {
826
1002
  workflowId?: string;
827
1003
  status?: 'canceled' | 'error' | 'running' | 'success' | 'waiting';
@@ -84,8 +84,8 @@ export interface NormalizedTriggerDraft {
84
84
  cronExpression?: string;
85
85
  eventKind?: string;
86
86
  maxRuns?: number;
87
- kind?: TriggerKind;
88
- workflowId?: string;
87
+ kind: TriggerKind;
88
+ workflowId: string;
89
89
  workflowName?: string;
90
90
  }
91
91
 
@@ -107,17 +107,6 @@ export interface TriggerExecutionResult {
107
107
  executionId?: string;
108
108
  }
109
109
 
110
- export interface TextTriggerWorkflowDraft {
111
- displayName: string;
112
- instructions: string;
113
- wakeMode: TriggerWakeMode;
114
- }
115
-
116
- export interface DeployedTriggerWorkflow {
117
- id: string;
118
- name: string;
119
- }
120
-
121
110
  interface TriggerDraftInput {
122
111
  displayName?: string;
123
112
  instructions?: string;
@@ -173,17 +162,6 @@ export interface TriggerRouteContext extends RouteRequestContext {
173
162
  input: TriggerDraftInput;
174
163
  fallback: NormalizeTriggerDraftFallback;
175
164
  }) => { draft?: NormalizedTriggerDraft; error?: string };
176
- /**
177
- * Phase 2E: every persisted trigger is `kind: "workflow"`. When the
178
- * caller submits `kind: "text"` (or omits `kind`), the route uses this
179
- * to materialize a single-node `respondToEvent` workflow first, then
180
- * stores the trigger as `kind: "workflow"` pointing at that workflow.
181
- */
182
- deployTextTriggerWorkflow: (
183
- runtime: IAgentRuntime,
184
- draft: TextTriggerWorkflowDraft,
185
- ownerId: string
186
- ) => Promise<DeployedTriggerWorkflow | null>;
187
165
  DISABLED_TRIGGER_INTERVAL_MS: number;
188
166
  TRIGGER_TASK_NAME: string;
189
167
  TRIGGER_TASK_TAGS: string[];
@@ -194,7 +172,7 @@ function trim(value: string): string {
194
172
  }
195
173
 
196
174
  function parseTriggerKind(value: unknown): TriggerKind | undefined {
197
- if (value === 'text' || value === 'workflow') return value;
175
+ if (value === 'workflow') return value;
198
176
  return undefined;
199
177
  }
200
178
 
@@ -202,8 +180,8 @@ type ParsedTriggerKind = { ok: true; kind: TriggerKind } | { ok: false; error: s
202
180
 
203
181
  function parseTriggerKindStrict(value: unknown): ParsedTriggerKind | undefined {
204
182
  if (value === undefined) return undefined;
205
- if (value === 'text' || value === 'workflow') return { ok: true, kind: value };
206
- return { ok: false, error: "kind must be 'text' or 'workflow'" };
183
+ if (value === 'workflow') return { ok: true, kind: value };
184
+ return { ok: false, error: "kind must be 'workflow'" };
207
185
  }
208
186
 
209
187
  function parseNonEmptyString(value: unknown): string | undefined {
@@ -276,7 +254,6 @@ export async function handleTriggerRoutes(ctx: TriggerRouteContext): Promise<boo
276
254
  buildTriggerConfig,
277
255
  buildTriggerMetadata,
278
256
  normalizeTriggerDraft,
279
- deployTextTriggerWorkflow,
280
257
  DISABLED_TRIGGER_INTERVAL_MS,
281
258
  TRIGGER_TASK_NAME,
282
259
  TRIGGER_TASK_TAGS,
@@ -315,7 +292,7 @@ export async function handleTriggerRoutes(ctx: TriggerRouteContext): Promise<boo
315
292
  const triggers = tasks
316
293
  .map(taskToTriggerSummary)
317
294
  .filter((summary): summary is TriggerSummary => summary !== null)
318
- .sort((a, b) => String(a.displayName ?? '').localeCompare(String(b.displayName ?? '')));
295
+ .sort((a, b) => String(a.displayName).localeCompare(String(b.displayName)));
319
296
  listResponse(triggers);
320
297
  return true;
321
298
  }
@@ -330,49 +307,14 @@ export async function handleTriggerRoutes(ctx: TriggerRouteContext): Promise<boo
330
307
  error(res, kindParsed.error, 400);
331
308
  return true;
332
309
  }
333
- const requestedKind: TriggerKind | undefined = kindParsed?.ok ? kindParsed.kind : undefined;
334
- let workflowId = parseNonEmptyString(body.workflowId);
335
- let workflowName = parseNonEmptyString(body.workflowName);
336
- if (requestedKind === 'workflow' && !workflowId) {
310
+ const requestedKind: TriggerKind = kindParsed?.ok ? kindParsed.kind : 'workflow';
311
+ const workflowId = parseNonEmptyString(body.workflowId);
312
+ const workflowName = parseNonEmptyString(body.workflowName);
313
+ if (!workflowId) {
337
314
  error(res, "workflowId is required when kind is 'workflow'", 400);
338
315
  return true;
339
316
  }
340
317
 
341
- // Phase 2E: when the client submits `kind: "text"` or omits `kind`,
342
- // materialize a single-node `respondToEvent` workflow up front so the
343
- // persisted trigger is always `kind: "workflow"`.
344
- if (requestedKind !== 'workflow') {
345
- const rawDisplayName =
346
- typeof body.displayName === 'string' && trim(body.displayName)
347
- ? trim(body.displayName)
348
- : 'New Trigger';
349
- const rawInstructions = typeof body.instructions === 'string' ? trim(body.instructions) : '';
350
- if (!rawInstructions) {
351
- error(res, 'instructions is required', 400);
352
- return true;
353
- }
354
- const wakeModeForWorkflow: TriggerWakeMode =
355
- typeof body.wakeMode === 'string' && body.wakeMode === 'next_autonomy_cycle'
356
- ? 'next_autonomy_cycle'
357
- : 'inject_now';
358
- const deployed = await deployTextTriggerWorkflow(
359
- runtime,
360
- {
361
- displayName: rawDisplayName,
362
- instructions: rawInstructions,
363
- wakeMode: wakeModeForWorkflow,
364
- },
365
- creator
366
- );
367
- if (!deployed) {
368
- error(res, 'Workflow plugin is not loaded; cannot create text triggers.', 503);
369
- return true;
370
- }
371
- workflowId = deployed.id;
372
- workflowName = deployed.name;
373
- }
374
-
375
- const kind: TriggerKind = 'workflow';
376
318
  const inputDraft: TriggerDraftInput = {
377
319
  displayName: typeof body.displayName === 'string' ? body.displayName : undefined,
378
320
  instructions: typeof body.instructions === 'string' ? body.instructions : undefined,
@@ -387,7 +329,7 @@ export async function handleTriggerRoutes(ctx: TriggerRouteContext): Promise<boo
387
329
  cronExpression: typeof body.cronExpression === 'string' ? body.cronExpression : undefined,
388
330
  eventKind: typeof body.eventKind === 'string' ? body.eventKind : undefined,
389
331
  maxRuns: typeof body.maxRuns === 'number' ? body.maxRuns : undefined,
390
- kind,
332
+ kind: requestedKind,
391
333
  workflowId,
392
334
  workflowName,
393
335
  };
@@ -615,7 +557,7 @@ export async function handleTriggerRoutes(ctx: TriggerRouteContext): Promise<boo
615
557
  return true;
616
558
  }
617
559
  const parsedKind: TriggerKind | undefined = kindParsed?.ok ? kindParsed.kind : undefined;
618
- const nextKind: TriggerKind | undefined = parsedKind ?? parseTriggerKind(current.kind);
560
+ const nextKind: TriggerKind = parsedKind ?? parseTriggerKind(current.kind) ?? 'workflow';
619
561
  const nextWorkflowId = parseNonEmptyString(body.workflowId) ?? current.workflowId;
620
562
  const nextWorkflowName = parseNonEmptyString(body.workflowName) ?? current.workflowName;
621
563
  if (nextKind === 'workflow' && !nextWorkflowId) {