@elizaos/plugin-workflow 2.0.0-beta.1

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 (294) hide show
  1. package/README.md +71 -0
  2. package/auto-enable.ts +18 -0
  3. package/dist/actions/index.d.ts +2 -0
  4. package/dist/actions/index.d.ts.map +1 -0
  5. package/dist/actions/index.js +2 -0
  6. package/dist/actions/index.js.map +1 -0
  7. package/dist/actions/workflow.d.ts +23 -0
  8. package/dist/actions/workflow.d.ts.map +1 -0
  9. package/dist/actions/workflow.js +425 -0
  10. package/dist/actions/workflow.js.map +1 -0
  11. package/dist/data/defaultNodes.json +9887 -0
  12. package/dist/data/schemaIndex.json +1 -0
  13. package/dist/data/triggerSchemaIndex.json +1 -0
  14. package/dist/db/index.d.ts +2 -0
  15. package/dist/db/index.d.ts.map +1 -0
  16. package/dist/db/index.js +2 -0
  17. package/dist/db/index.js.map +1 -0
  18. package/dist/db/schema.d.ts +588 -0
  19. package/dist/db/schema.d.ts.map +1 -0
  20. package/dist/db/schema.js +59 -0
  21. package/dist/db/schema.js.map +1 -0
  22. package/dist/index.d.ts +34 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +126 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/lib/automations-builder.d.ts +21 -0
  27. package/dist/lib/automations-builder.d.ts.map +1 -0
  28. package/dist/lib/automations-builder.js +557 -0
  29. package/dist/lib/automations-builder.js.map +1 -0
  30. package/dist/lib/automations-types.d.ts +153 -0
  31. package/dist/lib/automations-types.d.ts.map +1 -0
  32. package/dist/lib/automations-types.js +191 -0
  33. package/dist/lib/automations-types.js.map +1 -0
  34. package/dist/lib/index.d.ts +3 -0
  35. package/dist/lib/index.d.ts.map +1 -0
  36. package/dist/lib/index.js +3 -0
  37. package/dist/lib/index.js.map +1 -0
  38. package/dist/lib/legacy-task-migration.d.ts +20 -0
  39. package/dist/lib/legacy-task-migration.d.ts.map +1 -0
  40. package/dist/lib/legacy-task-migration.js +110 -0
  41. package/dist/lib/legacy-task-migration.js.map +1 -0
  42. package/dist/lib/legacy-text-trigger-migration.d.ts +18 -0
  43. package/dist/lib/legacy-text-trigger-migration.d.ts.map +1 -0
  44. package/dist/lib/legacy-text-trigger-migration.js +131 -0
  45. package/dist/lib/legacy-text-trigger-migration.js.map +1 -0
  46. package/dist/lib/workflow-clarification.d.ts +113 -0
  47. package/dist/lib/workflow-clarification.d.ts.map +1 -0
  48. package/dist/lib/workflow-clarification.js +425 -0
  49. package/dist/lib/workflow-clarification.js.map +1 -0
  50. package/dist/plugin-routes.d.ts +9 -0
  51. package/dist/plugin-routes.d.ts.map +1 -0
  52. package/dist/plugin-routes.js +147 -0
  53. package/dist/plugin-routes.js.map +1 -0
  54. package/dist/providers/activeWorkflows.d.ts +11 -0
  55. package/dist/providers/activeWorkflows.d.ts.map +1 -0
  56. package/dist/providers/activeWorkflows.js +72 -0
  57. package/dist/providers/activeWorkflows.js.map +1 -0
  58. package/dist/providers/index.d.ts +4 -0
  59. package/dist/providers/index.d.ts.map +1 -0
  60. package/dist/providers/index.js +4 -0
  61. package/dist/providers/index.js.map +1 -0
  62. package/dist/providers/pendingDraft.d.ts +9 -0
  63. package/dist/providers/pendingDraft.d.ts.map +1 -0
  64. package/dist/providers/pendingDraft.js +48 -0
  65. package/dist/providers/pendingDraft.js.map +1 -0
  66. package/dist/providers/workflowStatus.d.ts +3 -0
  67. package/dist/providers/workflowStatus.d.ts.map +1 -0
  68. package/dist/providers/workflowStatus.js +69 -0
  69. package/dist/providers/workflowStatus.js.map +1 -0
  70. package/dist/register-routes.d.ts +2 -0
  71. package/dist/register-routes.d.ts.map +1 -0
  72. package/dist/register-routes.js +6 -0
  73. package/dist/register-routes.js.map +1 -0
  74. package/dist/routes/_helpers.d.ts +11 -0
  75. package/dist/routes/_helpers.d.ts.map +1 -0
  76. package/dist/routes/_helpers.js +22 -0
  77. package/dist/routes/_helpers.js.map +1 -0
  78. package/dist/routes/automations.d.ts +19 -0
  79. package/dist/routes/automations.d.ts.map +1 -0
  80. package/dist/routes/automations.js +32 -0
  81. package/dist/routes/automations.js.map +1 -0
  82. package/dist/routes/embedded-webhooks.d.ts +3 -0
  83. package/dist/routes/embedded-webhooks.d.ts.map +1 -0
  84. package/dist/routes/embedded-webhooks.js +47 -0
  85. package/dist/routes/embedded-webhooks.js.map +1 -0
  86. package/dist/routes/executions.d.ts +3 -0
  87. package/dist/routes/executions.d.ts.map +1 -0
  88. package/dist/routes/executions.js +58 -0
  89. package/dist/routes/executions.js.map +1 -0
  90. package/dist/routes/index.d.ts +4 -0
  91. package/dist/routes/index.d.ts.map +1 -0
  92. package/dist/routes/index.js +14 -0
  93. package/dist/routes/index.js.map +1 -0
  94. package/dist/routes/nodes.d.ts +3 -0
  95. package/dist/routes/nodes.d.ts.map +1 -0
  96. package/dist/routes/nodes.js +168 -0
  97. package/dist/routes/nodes.js.map +1 -0
  98. package/dist/routes/validation.d.ts +3 -0
  99. package/dist/routes/validation.d.ts.map +1 -0
  100. package/dist/routes/validation.js +41 -0
  101. package/dist/routes/validation.js.map +1 -0
  102. package/dist/routes/workflow-routes.d.ts +27 -0
  103. package/dist/routes/workflow-routes.d.ts.map +1 -0
  104. package/dist/routes/workflow-routes.js +326 -0
  105. package/dist/routes/workflow-routes.js.map +1 -0
  106. package/dist/routes/workflows.d.ts +3 -0
  107. package/dist/routes/workflows.d.ts.map +1 -0
  108. package/dist/routes/workflows.js +252 -0
  109. package/dist/routes/workflows.js.map +1 -0
  110. package/dist/schemas/draftIntent.d.ts +22 -0
  111. package/dist/schemas/draftIntent.d.ts.map +1 -0
  112. package/dist/schemas/draftIntent.js +22 -0
  113. package/dist/schemas/draftIntent.js.map +1 -0
  114. package/dist/schemas/feasibility.d.ts +13 -0
  115. package/dist/schemas/feasibility.d.ts.map +1 -0
  116. package/dist/schemas/feasibility.js +9 -0
  117. package/dist/schemas/feasibility.js.map +1 -0
  118. package/dist/schemas/index.d.ts +5 -0
  119. package/dist/schemas/index.d.ts.map +1 -0
  120. package/dist/schemas/index.js +5 -0
  121. package/dist/schemas/index.js.map +1 -0
  122. package/dist/schemas/keywordExtraction.d.ts +14 -0
  123. package/dist/schemas/keywordExtraction.d.ts.map +1 -0
  124. package/dist/schemas/keywordExtraction.js +12 -0
  125. package/dist/schemas/keywordExtraction.js.map +1 -0
  126. package/dist/schemas/workflowMatching.d.ts +36 -0
  127. package/dist/schemas/workflowMatching.d.ts.map +1 -0
  128. package/dist/schemas/workflowMatching.js +30 -0
  129. package/dist/schemas/workflowMatching.js.map +1 -0
  130. package/dist/services/embedded-workflow-service.d.ts +106 -0
  131. package/dist/services/embedded-workflow-service.d.ts.map +1 -0
  132. package/dist/services/embedded-workflow-service.js +1900 -0
  133. package/dist/services/embedded-workflow-service.js.map +1 -0
  134. package/dist/services/index.d.ts +5 -0
  135. package/dist/services/index.d.ts.map +1 -0
  136. package/dist/services/index.js +5 -0
  137. package/dist/services/index.js.map +1 -0
  138. package/dist/services/workflow-credential-store.d.ts +27 -0
  139. package/dist/services/workflow-credential-store.d.ts.map +1 -0
  140. package/dist/services/workflow-credential-store.js +92 -0
  141. package/dist/services/workflow-credential-store.js.map +1 -0
  142. package/dist/services/workflow-dispatch.d.ts +41 -0
  143. package/dist/services/workflow-dispatch.d.ts.map +1 -0
  144. package/dist/services/workflow-dispatch.js +86 -0
  145. package/dist/services/workflow-dispatch.js.map +1 -0
  146. package/dist/services/workflow-service.d.ts +63 -0
  147. package/dist/services/workflow-service.d.ts.map +1 -0
  148. package/dist/services/workflow-service.js +492 -0
  149. package/dist/services/workflow-service.js.map +1 -0
  150. package/dist/trigger-routes.d.ts +153 -0
  151. package/dist/trigger-routes.d.ts.map +1 -0
  152. package/dist/trigger-routes.js +424 -0
  153. package/dist/trigger-routes.js.map +1 -0
  154. package/dist/types/index.d.ts +457 -0
  155. package/dist/types/index.d.ts.map +1 -0
  156. package/dist/types/index.js +59 -0
  157. package/dist/types/index.js.map +1 -0
  158. package/dist/utils/catalog.d.ts +16 -0
  159. package/dist/utils/catalog.d.ts.map +1 -0
  160. package/dist/utils/catalog.js +211 -0
  161. package/dist/utils/catalog.js.map +1 -0
  162. package/dist/utils/clarification.d.ts +17 -0
  163. package/dist/utils/clarification.d.ts.map +1 -0
  164. package/dist/utils/clarification.js +46 -0
  165. package/dist/utils/clarification.js.map +1 -0
  166. package/dist/utils/context.d.ts +4 -0
  167. package/dist/utils/context.d.ts.map +1 -0
  168. package/dist/utils/context.js +18 -0
  169. package/dist/utils/context.js.map +1 -0
  170. package/dist/utils/credentialResolver.d.ts +22 -0
  171. package/dist/utils/credentialResolver.d.ts.map +1 -0
  172. package/dist/utils/credentialResolver.js +146 -0
  173. package/dist/utils/credentialResolver.js.map +1 -0
  174. package/dist/utils/generation.d.ts +36 -0
  175. package/dist/utils/generation.d.ts.map +1 -0
  176. package/dist/utils/generation.js +701 -0
  177. package/dist/utils/generation.js.map +1 -0
  178. package/dist/utils/host-capabilities.d.ts +27 -0
  179. package/dist/utils/host-capabilities.d.ts.map +1 -0
  180. package/dist/utils/host-capabilities.js +59 -0
  181. package/dist/utils/host-capabilities.js.map +1 -0
  182. package/dist/utils/inferSyntheticOutputSchema.d.ts +20 -0
  183. package/dist/utils/inferSyntheticOutputSchema.d.ts.map +1 -0
  184. package/dist/utils/inferSyntheticOutputSchema.js +151 -0
  185. package/dist/utils/inferSyntheticOutputSchema.js.map +1 -0
  186. package/dist/utils/outputSchema.d.ts +26 -0
  187. package/dist/utils/outputSchema.d.ts.map +1 -0
  188. package/dist/utils/outputSchema.js +297 -0
  189. package/dist/utils/outputSchema.js.map +1 -0
  190. package/dist/utils/validateAndRepair.d.ts +41 -0
  191. package/dist/utils/validateAndRepair.d.ts.map +1 -0
  192. package/dist/utils/validateAndRepair.js +483 -0
  193. package/dist/utils/validateAndRepair.js.map +1 -0
  194. package/dist/utils/workflow-prompts/actionResponse.d.ts +2 -0
  195. package/dist/utils/workflow-prompts/actionResponse.d.ts.map +1 -0
  196. package/dist/utils/workflow-prompts/actionResponse.js +17 -0
  197. package/dist/utils/workflow-prompts/actionResponse.js.map +1 -0
  198. package/dist/utils/workflow-prompts/draftIntent.d.ts +2 -0
  199. package/dist/utils/workflow-prompts/draftIntent.d.ts.map +1 -0
  200. package/dist/utils/workflow-prompts/draftIntent.js +23 -0
  201. package/dist/utils/workflow-prompts/draftIntent.js.map +1 -0
  202. package/dist/utils/workflow-prompts/feasibilityCheck.d.ts +2 -0
  203. package/dist/utils/workflow-prompts/feasibilityCheck.d.ts.map +1 -0
  204. package/dist/utils/workflow-prompts/feasibilityCheck.js +21 -0
  205. package/dist/utils/workflow-prompts/feasibilityCheck.js.map +1 -0
  206. package/dist/utils/workflow-prompts/fieldCorrection.d.ts +3 -0
  207. package/dist/utils/workflow-prompts/fieldCorrection.d.ts.map +1 -0
  208. package/dist/utils/workflow-prompts/fieldCorrection.js +20 -0
  209. package/dist/utils/workflow-prompts/fieldCorrection.js.map +1 -0
  210. package/dist/utils/workflow-prompts/index.d.ts +8 -0
  211. package/dist/utils/workflow-prompts/index.d.ts.map +1 -0
  212. package/dist/utils/workflow-prompts/index.js +8 -0
  213. package/dist/utils/workflow-prompts/index.js.map +1 -0
  214. package/dist/utils/workflow-prompts/keywordExtraction.d.ts +2 -0
  215. package/dist/utils/workflow-prompts/keywordExtraction.d.ts.map +1 -0
  216. package/dist/utils/workflow-prompts/keywordExtraction.js +21 -0
  217. package/dist/utils/workflow-prompts/keywordExtraction.js.map +1 -0
  218. package/dist/utils/workflow-prompts/parameterCorrection.d.ts +3 -0
  219. package/dist/utils/workflow-prompts/parameterCorrection.d.ts.map +1 -0
  220. package/dist/utils/workflow-prompts/parameterCorrection.js +29 -0
  221. package/dist/utils/workflow-prompts/parameterCorrection.js.map +1 -0
  222. package/dist/utils/workflow-prompts/workflowGeneration.d.ts +2 -0
  223. package/dist/utils/workflow-prompts/workflowGeneration.d.ts.map +1 -0
  224. package/dist/utils/workflow-prompts/workflowGeneration.js +529 -0
  225. package/dist/utils/workflow-prompts/workflowGeneration.js.map +1 -0
  226. package/dist/utils/workflow-prompts/workflowMatching.d.ts +2 -0
  227. package/dist/utils/workflow-prompts/workflowMatching.d.ts.map +1 -0
  228. package/dist/utils/workflow-prompts/workflowMatching.js +23 -0
  229. package/dist/utils/workflow-prompts/workflowMatching.js.map +1 -0
  230. package/dist/utils/workflow.d.ts +62 -0
  231. package/dist/utils/workflow.d.ts.map +1 -0
  232. package/dist/utils/workflow.js +712 -0
  233. package/dist/utils/workflow.js.map +1 -0
  234. package/package.json +87 -0
  235. package/src/actions/index.ts +1 -0
  236. package/src/actions/workflow.ts +494 -0
  237. package/src/data/defaultNodes.json +9887 -0
  238. package/src/data/schemaIndex.json +1 -0
  239. package/src/data/triggerSchemaIndex.json +1 -0
  240. package/src/db/index.ts +8 -0
  241. package/src/db/schema.ts +94 -0
  242. package/src/index.ts +179 -0
  243. package/src/lib/automations-builder.ts +679 -0
  244. package/src/lib/automations-types.ts +391 -0
  245. package/src/lib/index.ts +8 -0
  246. package/src/lib/legacy-task-migration.ts +143 -0
  247. package/src/lib/legacy-text-trigger-migration.ts +178 -0
  248. package/src/lib/workflow-clarification.ts +497 -0
  249. package/src/plugin-routes.ts +164 -0
  250. package/src/providers/activeWorkflows.ts +81 -0
  251. package/src/providers/index.ts +3 -0
  252. package/src/providers/pendingDraft.ts +55 -0
  253. package/src/providers/workflowStatus.ts +88 -0
  254. package/src/register-routes.ts +6 -0
  255. package/src/routes/_helpers.ts +27 -0
  256. package/src/routes/automations.ts +46 -0
  257. package/src/routes/embedded-webhooks.ts +64 -0
  258. package/src/routes/executions.ts +75 -0
  259. package/src/routes/index.ts +16 -0
  260. package/src/routes/nodes.ts +211 -0
  261. package/src/routes/validation.ts +51 -0
  262. package/src/routes/workflow-routes.ts +469 -0
  263. package/src/routes/workflows.ts +310 -0
  264. package/src/schemas/draftIntent.ts +21 -0
  265. package/src/schemas/feasibility.ts +8 -0
  266. package/src/schemas/index.ts +4 -0
  267. package/src/schemas/keywordExtraction.ts +11 -0
  268. package/src/schemas/workflowMatching.ts +29 -0
  269. package/src/services/embedded-workflow-service.ts +2224 -0
  270. package/src/services/index.ts +17 -0
  271. package/src/services/workflow-credential-store.ts +132 -0
  272. package/src/services/workflow-dispatch.ts +121 -0
  273. package/src/services/workflow-service.ts +839 -0
  274. package/src/trigger-routes.ts +714 -0
  275. package/src/types/index.ts +562 -0
  276. package/src/utils/catalog.ts +260 -0
  277. package/src/utils/clarification.ts +52 -0
  278. package/src/utils/context.ts +22 -0
  279. package/src/utils/credentialResolver.ts +234 -0
  280. package/src/utils/generation.ts +987 -0
  281. package/src/utils/host-capabilities.ts +81 -0
  282. package/src/utils/inferSyntheticOutputSchema.ts +163 -0
  283. package/src/utils/outputSchema.ts +372 -0
  284. package/src/utils/validateAndRepair.ts +610 -0
  285. package/src/utils/workflow-prompts/actionResponse.ts +16 -0
  286. package/src/utils/workflow-prompts/draftIntent.ts +22 -0
  287. package/src/utils/workflow-prompts/feasibilityCheck.ts +20 -0
  288. package/src/utils/workflow-prompts/fieldCorrection.ts +20 -0
  289. package/src/utils/workflow-prompts/index.ts +10 -0
  290. package/src/utils/workflow-prompts/keywordExtraction.ts +20 -0
  291. package/src/utils/workflow-prompts/parameterCorrection.ts +29 -0
  292. package/src/utils/workflow-prompts/workflowGeneration.ts +528 -0
  293. package/src/utils/workflow-prompts/workflowMatching.ts +22 -0
  294. package/src/utils/workflow.ts +895 -0
@@ -0,0 +1,679 @@
1
+ /**
2
+ * Builder for the `/api/automations` response surface.
3
+ *
4
+ * Reads workflows in-process via WorkflowService, runtime tasks via the core
5
+ * runtime task API, and draft conversations via runtime room APIs. There is
6
+ * no dynamic import of plugin-workflow because this code IS the plugin.
7
+ *
8
+ * Trigger task lookup: the runtime stores trigger tasks under three tag
9
+ * combinations — `["queue","repeat","trigger"]` for user-defined triggers and
10
+ * `["queue","repeat","heartbeat"]` for plugin-owned heartbeats. We replicate
11
+ * `listTriggerTasks` from `packages/agent/src/triggers/runtime.ts` here
12
+ * because plugin-workflow cannot depend on @elizaos/agent.
13
+ */
14
+
15
+ import type { AgentRuntime, Room, Task, UUID } from '@elizaos/core';
16
+ import { stringToUuid } from '@elizaos/core';
17
+ import type { WorkflowStatusResponse } from '../routes/workflow-routes';
18
+ import { WORKFLOW_SERVICE_TYPE, type WorkflowService } from '../services/workflow-service';
19
+ import type {
20
+ WorkflowDefinition,
21
+ WorkflowDefinitionResponse,
22
+ WorkflowExecution,
23
+ } from '../types/index';
24
+ import {
25
+ type AutomationItem,
26
+ type AutomationLastExecution,
27
+ type AutomationListResponse,
28
+ type AutomationRoomBinding,
29
+ type AutomationSummary,
30
+ type ConversationMetadata,
31
+ type ConversationScope,
32
+ isAutomationConversationMetadata,
33
+ type TriggerSummary,
34
+ taskToTriggerSummary,
35
+ toWorkbenchTaskView,
36
+ type WorkbenchTaskView,
37
+ } from './automations-types';
38
+
39
+ const WORKFLOW_DRAFT_TITLE = 'New Workflow Draft';
40
+
41
+ const SYSTEM_TASK_NAMES = new Set([
42
+ 'EMBEDDING_DRAIN',
43
+ 'PROACTIVE_AGENT',
44
+ 'LIFEOPS_SCHEDULER',
45
+ 'TRIGGER_DISPATCH',
46
+ 'heartbeat',
47
+ ]);
48
+
49
+ // 30s cache for last-execution data — avoids hammering the workflow runtime on
50
+ // every automations poll. null data = checked and found no executions yet
51
+ // (still cached to avoid re-polling).
52
+ const lastExecutionCache = new Map<
53
+ string,
54
+ { data: AutomationLastExecution | null; expiresAt: number }
55
+ >();
56
+ const LAST_EXECUTION_TTL_MS = 30_000;
57
+
58
+ interface AutomationRoomRecord {
59
+ title: string;
60
+ roomId: string;
61
+ conversationId: string | null;
62
+ metadata: ConversationMetadata;
63
+ updatedAt: string | null;
64
+ }
65
+
66
+ function isRecord(value: unknown): value is Record<string, unknown> {
67
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
68
+ }
69
+
70
+ function asString(value: unknown): string | undefined {
71
+ if (typeof value !== 'string') {
72
+ return undefined;
73
+ }
74
+ const trimmed = value.trim();
75
+ return trimmed.length > 0 ? trimmed : undefined;
76
+ }
77
+
78
+ function normalizeDateValue(value: unknown): string | null {
79
+ if (typeof value === 'string') {
80
+ const parsed = Date.parse(value);
81
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
82
+ }
83
+ if (typeof value === 'number' && Number.isFinite(value)) {
84
+ return new Date(value).toISOString();
85
+ }
86
+ if (value instanceof Date) {
87
+ return value.toISOString();
88
+ }
89
+ return null;
90
+ }
91
+
92
+ function resolveAgentName(runtime: AgentRuntime): string {
93
+ return runtime.character?.name?.trim() || 'Eliza';
94
+ }
95
+
96
+ function isSystemTask(task: WorkbenchTaskView): boolean {
97
+ if (SYSTEM_TASK_NAMES.has(task.name)) {
98
+ return true;
99
+ }
100
+ const tags = new Set(task.tags ?? []);
101
+ return tags.has('queue') && tags.has('repeat');
102
+ }
103
+
104
+ /**
105
+ * True when the raw runtime task carries the `migratedToWorkflowId` metadata
106
+ * flag set by `migrateLegacyWorkbenchTasks`. The workflow representation is
107
+ * already surfaced by the workflow listing path; the original task should not
108
+ * also appear as a coordinator-text item.
109
+ */
110
+ function taskHasMigrationFlag(rawTasks: Task[], taskId: string): boolean {
111
+ for (const raw of rawTasks) {
112
+ if (raw.id !== taskId) continue;
113
+ const meta = isRecord(raw.metadata) ? raw.metadata : null;
114
+ if (meta && typeof meta.migratedToWorkflowId === 'string') {
115
+ return true;
116
+ }
117
+ return false;
118
+ }
119
+ return false;
120
+ }
121
+
122
+ function choosePreferredSystemTask(
123
+ current: WorkbenchTaskView,
124
+ candidate: WorkbenchTaskView
125
+ ): WorkbenchTaskView {
126
+ const currentHasDescription = current.description.trim().length > 0;
127
+ const candidateHasDescription = candidate.description.trim().length > 0;
128
+ if (candidateHasDescription && !currentHasDescription) {
129
+ return candidate;
130
+ }
131
+ if (currentHasDescription && !candidateHasDescription) {
132
+ return current;
133
+ }
134
+ return (candidate.updatedAt ?? 0) > (current.updatedAt ?? 0) ? candidate : current;
135
+ }
136
+
137
+ function deduplicateSystemTasks(tasks: WorkbenchTaskView[]): WorkbenchTaskView[] {
138
+ const systemTasksByName = new Map<string, WorkbenchTaskView>();
139
+ const userTasks: WorkbenchTaskView[] = [];
140
+
141
+ for (const task of tasks) {
142
+ if (!isSystemTask(task)) {
143
+ userTasks.push(task);
144
+ continue;
145
+ }
146
+ const existing = systemTasksByName.get(task.name);
147
+ if (!existing) {
148
+ systemTasksByName.set(task.name, task);
149
+ continue;
150
+ }
151
+ systemTasksByName.set(task.name, choosePreferredSystemTask(existing, task));
152
+ }
153
+
154
+ return [...userTasks, ...systemTasksByName.values()];
155
+ }
156
+
157
+ function buildRoomBinding(room: AutomationRoomRecord | undefined): AutomationRoomBinding | null {
158
+ if (!room) {
159
+ return null;
160
+ }
161
+ return {
162
+ conversationId: room.conversationId,
163
+ roomId: room.roomId,
164
+ scope: (room.metadata.scope ?? 'general') as ConversationScope,
165
+ ...(room.metadata.sourceConversationId
166
+ ? { sourceConversationId: room.metadata.sourceConversationId }
167
+ : {}),
168
+ ...(room.metadata.terminalBridgeConversationId
169
+ ? {
170
+ terminalBridgeConversationId: room.metadata.terminalBridgeConversationId,
171
+ }
172
+ : {}),
173
+ };
174
+ }
175
+
176
+ function extractConversationMetadataFromRoom(
177
+ room: Pick<Room, 'metadata'> | null | undefined
178
+ ): ConversationMetadata | undefined {
179
+ const roomMetadata = isRecord(room?.metadata) ? room.metadata : null;
180
+ if (!roomMetadata) {
181
+ return undefined;
182
+ }
183
+ const stored = isRecord(roomMetadata.webConversation) ? roomMetadata.webConversation : null;
184
+ if (!stored) {
185
+ return undefined;
186
+ }
187
+ // The full sanitization lives in @elizaos/agent; here we just extract the
188
+ // metadata fields we actually consume in the automations response.
189
+ const next: ConversationMetadata = {};
190
+ const scope = asString(stored.scope);
191
+ if (scope) next.scope = scope as ConversationScope;
192
+ const automationType = asString(stored.automationType);
193
+ if (automationType === 'coordinator_text' || automationType === 'workflow') {
194
+ next.automationType = automationType;
195
+ }
196
+ const taskId = asString(stored.taskId);
197
+ if (taskId) next.taskId = taskId;
198
+ const triggerId = asString(stored.triggerId);
199
+ if (triggerId) next.triggerId = triggerId;
200
+ const workflowId = asString(stored.workflowId);
201
+ if (workflowId) next.workflowId = workflowId;
202
+ const workflowName = asString(stored.workflowName);
203
+ if (workflowName) next.workflowName = workflowName;
204
+ const draftId = asString(stored.draftId);
205
+ if (draftId) next.draftId = draftId;
206
+ const sourceConversationId = asString(stored.sourceConversationId);
207
+ if (sourceConversationId) next.sourceConversationId = sourceConversationId;
208
+ const terminalBridgeConversationId = asString(stored.terminalBridgeConversationId);
209
+ if (terminalBridgeConversationId)
210
+ next.terminalBridgeConversationId = terminalBridgeConversationId;
211
+ return Object.keys(next).length > 0 ? next : undefined;
212
+ }
213
+
214
+ function readAutomationRoomRecord(
215
+ room: Room & { updatedAt?: unknown }
216
+ ): AutomationRoomRecord | null {
217
+ const roomId = asString(room.id);
218
+ if (!roomId) {
219
+ return null;
220
+ }
221
+
222
+ const metadata = extractConversationMetadataFromRoom(room);
223
+ if (!metadata || !isAutomationConversationMetadata(metadata)) {
224
+ return null;
225
+ }
226
+
227
+ const roomMetadata = isRecord(room.metadata) ? room.metadata : null;
228
+ const webConversation = isRecord(roomMetadata?.webConversation)
229
+ ? roomMetadata.webConversation
230
+ : null;
231
+
232
+ return {
233
+ title: asString(room.name) ?? 'Automation',
234
+ roomId,
235
+ conversationId: asString(webConversation?.conversationId) ?? null,
236
+ metadata,
237
+ updatedAt: normalizeDateValue(room.updatedAt),
238
+ };
239
+ }
240
+
241
+ async function listAutomationRooms(
242
+ runtime: AgentRuntime,
243
+ agentName: string
244
+ ): Promise<AutomationRoomRecord[]> {
245
+ const worldId = stringToUuid(`${agentName}-web-chat-world`) as UUID;
246
+ const rooms = await runtime.getRooms(worldId);
247
+ return rooms
248
+ .map((room) => readAutomationRoomRecord(room))
249
+ .filter((room): room is AutomationRoomRecord => room !== null);
250
+ }
251
+
252
+ /**
253
+ * Replicates `listTriggerTasks` from @elizaos/agent's triggers/runtime.ts.
254
+ * We can't import from @elizaos/agent (would create a dependency cycle),
255
+ * so we hit the runtime's task API directly with the same tag filters.
256
+ * `runtime.getTasks` automatically scopes by `agentId`.
257
+ */
258
+ async function listTriggerTasks(runtime: AgentRuntime): Promise<Task[]> {
259
+ const [triggerTasks, heartbeatTasks] = await Promise.all([
260
+ runtime.getTasks({ tags: ['repeat', 'trigger'] }),
261
+ runtime.getTasks({ tags: ['repeat', 'heartbeat'] }),
262
+ ]);
263
+
264
+ const merged = new Map<string, Task>();
265
+ for (const task of [...triggerTasks, ...heartbeatTasks]) {
266
+ const key =
267
+ task.id ?? `${task.name ?? ''}:${task.description ?? ''}:${(task.tags ?? []).join(',')}`;
268
+ if (!merged.has(key)) {
269
+ merged.set(key, task);
270
+ }
271
+ }
272
+ return [...merged.values()];
273
+ }
274
+
275
+ function buildCoordinatorTaskItem(
276
+ task: WorkbenchTaskView,
277
+ room: AutomationRoomRecord | undefined
278
+ ): AutomationItem {
279
+ const system = isSystemTask(task);
280
+ return {
281
+ id: `task:${task.id}`,
282
+ type: 'coordinator_text',
283
+ source: 'workbench_task',
284
+ title: task.name,
285
+ description: task.description,
286
+ status: system ? 'system' : task.isCompleted ? 'completed' : 'active',
287
+ enabled: !task.isCompleted,
288
+ system,
289
+ isDraft: false,
290
+ hasBackingWorkflow: false,
291
+ updatedAt: room?.updatedAt ?? normalizeDateValue(task.updatedAt),
292
+ taskId: task.id,
293
+ task,
294
+ schedules: [],
295
+ room: buildRoomBinding(room),
296
+ };
297
+ }
298
+
299
+ function buildCoordinatorTriggerItem(
300
+ trigger: TriggerSummary,
301
+ room: AutomationRoomRecord | undefined
302
+ ): AutomationItem {
303
+ return {
304
+ id: `trigger:${trigger.id}`,
305
+ type: 'coordinator_text',
306
+ source: 'trigger',
307
+ title: trigger.displayName,
308
+ description: trigger.instructions,
309
+ status: trigger.enabled ? 'active' : 'paused',
310
+ enabled: trigger.enabled,
311
+ system: false,
312
+ isDraft: false,
313
+ hasBackingWorkflow: false,
314
+ updatedAt:
315
+ room?.updatedAt ??
316
+ normalizeDateValue(trigger.updatedAt) ??
317
+ normalizeDateValue(trigger.lastRunAtIso),
318
+ triggerId: trigger.id,
319
+ trigger,
320
+ schedules: [trigger],
321
+ room: buildRoomBinding(room),
322
+ };
323
+ }
324
+
325
+ function buildWorkflowDraftItem(room: AutomationRoomRecord): AutomationItem {
326
+ const metadata = room.metadata;
327
+ const title = metadata.workflowName?.trim() || room.title.trim() || WORKFLOW_DRAFT_TITLE;
328
+ return {
329
+ id: `workflow-draft:${metadata.draftId}`,
330
+ type: 'workflow',
331
+ source: 'workflow_draft',
332
+ title,
333
+ description: '',
334
+ status: 'draft',
335
+ enabled: true,
336
+ system: false,
337
+ isDraft: true,
338
+ hasBackingWorkflow: false,
339
+ updatedAt: room.updatedAt,
340
+ draftId: room.metadata.draftId,
341
+ schedules: [],
342
+ room: buildRoomBinding(room),
343
+ };
344
+ }
345
+
346
+ function buildAutomationDraftItem(room: AutomationRoomRecord): AutomationItem {
347
+ const metadata = room.metadata;
348
+ const trimmedTitle = room.title.trim();
349
+ const title =
350
+ trimmedTitle && trimmedTitle.toLowerCase() !== 'default' ? trimmedTitle : 'New automation';
351
+ return {
352
+ id: `automation-draft:${metadata.draftId}`,
353
+ type: 'automation_draft',
354
+ source: 'automation_draft',
355
+ title,
356
+ description: '',
357
+ status: 'draft',
358
+ enabled: true,
359
+ system: false,
360
+ isDraft: true,
361
+ hasBackingWorkflow: false,
362
+ updatedAt: room.updatedAt,
363
+ draftId: metadata.draftId,
364
+ schedules: [],
365
+ room: buildRoomBinding(room),
366
+ };
367
+ }
368
+
369
+ function buildWorkflowItem(
370
+ workflow: WorkflowDefinition | undefined,
371
+ room: AutomationRoomRecord | undefined,
372
+ fallback: {
373
+ workflowId: string;
374
+ workflowName?: string;
375
+ trigger?: TriggerSummary;
376
+ }
377
+ ): AutomationItem {
378
+ const missingBackingWorkflow = !workflow && !fallback.trigger;
379
+ const title =
380
+ workflow?.name?.trim() ||
381
+ room?.metadata.workflowName?.trim() ||
382
+ fallback.workflowName?.trim() ||
383
+ fallback.workflowId;
384
+ const enabled =
385
+ missingBackingWorkflow === true
386
+ ? false
387
+ : (workflow?.active ?? fallback.trigger?.enabled ?? false);
388
+ const description =
389
+ (workflow as { description?: string } | undefined)?.description?.trim() ||
390
+ (fallback.trigger ? `Scheduled workflow automation for ${title}.` : '');
391
+
392
+ return {
393
+ id: `workflow:${fallback.workflowId}`,
394
+ type: 'workflow',
395
+ source: workflow ? 'workflow' : 'workflow_shadow',
396
+ title,
397
+ description,
398
+ status: missingBackingWorkflow ? 'draft' : enabled ? 'active' : 'paused',
399
+ enabled,
400
+ system: false,
401
+ isDraft: missingBackingWorkflow,
402
+ hasBackingWorkflow: Boolean(workflow),
403
+ updatedAt:
404
+ room?.updatedAt ??
405
+ normalizeDateValue(fallback.trigger?.updatedAt) ??
406
+ normalizeDateValue(fallback.trigger?.lastRunAtIso),
407
+ workflowId: fallback.workflowId,
408
+ workflow,
409
+ schedules: fallback.trigger ? [fallback.trigger] : [],
410
+ room: buildRoomBinding(room),
411
+ };
412
+ }
413
+
414
+ function compareAutomationItems(left: AutomationItem, right: AutomationItem): number {
415
+ if (left.system !== right.system) {
416
+ return left.system ? 1 : -1;
417
+ }
418
+ if (left.isDraft !== right.isDraft) {
419
+ return left.isDraft ? -1 : 1;
420
+ }
421
+ const leftUpdated = left.updatedAt ? Date.parse(left.updatedAt) : 0;
422
+ const rightUpdated = right.updatedAt ? Date.parse(right.updatedAt) : 0;
423
+ if (rightUpdated !== leftUpdated) {
424
+ return rightUpdated - leftUpdated;
425
+ }
426
+ return left.title.localeCompare(right.title);
427
+ }
428
+
429
+ function normalizeLastExecution(raw: WorkflowExecution): AutomationLastExecution | null {
430
+ const rawStatus = raw.status;
431
+ if (typeof rawStatus !== 'string') return null;
432
+ const STATUS_MAP: Record<string, AutomationLastExecution['status']> = {
433
+ success: 'success',
434
+ error: 'error',
435
+ crashed: 'error',
436
+ running: 'running',
437
+ waiting: 'waiting',
438
+ };
439
+ const status = STATUS_MAP[rawStatus] ?? 'unknown';
440
+ const startedAt = typeof raw.startedAt === 'string' ? raw.startedAt : null;
441
+ if (!startedAt) return null;
442
+ const stoppedAt = typeof raw.stoppedAt === 'string' ? raw.stoppedAt : null;
443
+ const errorMessage = (() => {
444
+ const data = isRecord(raw.data) ? raw.data : null;
445
+ const resultData = isRecord(data?.resultData) ? data.resultData : null;
446
+ const error = isRecord(resultData?.error) ? resultData.error : null;
447
+ return typeof error?.message === 'string' ? error.message : undefined;
448
+ })();
449
+ return {
450
+ status,
451
+ startedAt,
452
+ stoppedAt,
453
+ ...(errorMessage ? { errorMessage } : {}),
454
+ };
455
+ }
456
+
457
+ function getWorkflowService(runtime: AgentRuntime): WorkflowService | null {
458
+ const candidate = runtime.getService?.(WORKFLOW_SERVICE_TYPE);
459
+ return (candidate as WorkflowService | null) ?? null;
460
+ }
461
+
462
+ function buildWorkflowStatus(service: WorkflowService | null): WorkflowStatusResponse {
463
+ return {
464
+ mode: service ? 'local' : 'disabled',
465
+ host: 'in-process',
466
+ status: service ? 'ready' : 'error',
467
+ cloudConnected: false,
468
+ localEnabled: Boolean(service),
469
+ platform: 'desktop',
470
+ cloudHealth: 'unknown',
471
+ errorMessage: service ? null : 'Workflow service is not registered',
472
+ };
473
+ }
474
+
475
+ async function loadWorkflowList(service: WorkflowService | null): Promise<{
476
+ workflows: WorkflowDefinitionResponse[];
477
+ workflowFetchError: string | null;
478
+ }> {
479
+ if (!service) {
480
+ return { workflows: [], workflowFetchError: 'Workflow service is not registered' };
481
+ }
482
+ try {
483
+ const workflows = await service.listWorkflows();
484
+ return { workflows, workflowFetchError: null };
485
+ } catch (error) {
486
+ return {
487
+ workflows: [],
488
+ workflowFetchError: error instanceof Error ? error.message : 'Unable to load workflows',
489
+ };
490
+ }
491
+ }
492
+
493
+ async function fetchLastExecution(
494
+ service: WorkflowService,
495
+ workflowId: string
496
+ ): Promise<AutomationLastExecution | null> {
497
+ const cached = lastExecutionCache.get(workflowId);
498
+ if (cached && cached.expiresAt > Date.now()) {
499
+ return cached.data;
500
+ }
501
+ const response = await service.listExecutions({ workflowId, limit: 1 });
502
+ if (!response.data || response.data.length === 0) {
503
+ lastExecutionCache.set(workflowId, {
504
+ data: null,
505
+ expiresAt: Date.now() + LAST_EXECUTION_TTL_MS,
506
+ });
507
+ return null;
508
+ }
509
+ const exec = normalizeLastExecution(response.data[0]);
510
+ lastExecutionCache.set(workflowId, {
511
+ data: exec,
512
+ expiresAt: Date.now() + LAST_EXECUTION_TTL_MS,
513
+ });
514
+ return exec;
515
+ }
516
+
517
+ export async function buildAutomationListResponse(
518
+ runtime: AgentRuntime
519
+ ): Promise<AutomationListResponse> {
520
+ const agentName = resolveAgentName(runtime);
521
+ const rooms = await listAutomationRooms(runtime, agentName);
522
+ const taskRooms = new Map(
523
+ rooms
524
+ .filter((room) => room.metadata.taskId)
525
+ .map((room) => [room.metadata.taskId as string, room])
526
+ );
527
+ const triggerRooms = new Map(
528
+ rooms
529
+ .filter((room) => room.metadata.triggerId)
530
+ .map((room) => [room.metadata.triggerId as string, room])
531
+ );
532
+ const workflowRooms = new Map(
533
+ rooms
534
+ .filter((room) => room.metadata.workflowId)
535
+ .map((room) => [room.metadata.workflowId as string, room])
536
+ );
537
+ const workflowDraftItems = rooms
538
+ .filter((room) => room.metadata.scope === 'automation-workflow-draft')
539
+ .filter((room) => typeof room.metadata.draftId === 'string')
540
+ .map((room) => buildWorkflowDraftItem(room));
541
+ const automationDraftItems = rooms
542
+ .filter((room) => room.metadata.scope === 'automation-draft')
543
+ .filter((room) => typeof room.metadata.draftId === 'string')
544
+ .map((room) => buildAutomationDraftItem(room));
545
+
546
+ const allTasks = await runtime.getTasks({});
547
+ const tasks = deduplicateSystemTasks(
548
+ allTasks
549
+ .map((task) => toWorkbenchTaskView(task))
550
+ .filter((task): task is WorkbenchTaskView => task !== null)
551
+ // Tasks migrated to workflows by plugin-workflow's boot migration carry
552
+ // a `migratedToWorkflowId` metadata flag; their workflow representation
553
+ // is already in the workflowItemsById map below, so skip them here to
554
+ // avoid duplicate Automations entries.
555
+ .filter((task) => !taskHasMigrationFlag(allTasks, task.id))
556
+ );
557
+
558
+ const triggerTaskRecords = await listTriggerTasks(runtime);
559
+ const triggerItems = triggerTaskRecords
560
+ .map((task) => taskToTriggerSummary(task))
561
+ .filter((trigger): trigger is TriggerSummary => trigger !== null);
562
+ const triggerTaskIds = new Set(triggerItems.map((trigger) => trigger.taskId));
563
+ const taskItems = tasks
564
+ .filter((task) => !triggerTaskIds.has(task.id))
565
+ .map((task) => buildCoordinatorTaskItem(task, taskRooms.get(task.id)));
566
+
567
+ const service = getWorkflowService(runtime);
568
+ const workflowStatus = buildWorkflowStatus(service);
569
+ const { workflows: workflowList, workflowFetchError } = await loadWorkflowList(service);
570
+
571
+ const workflowItemsById = new Map<string, AutomationItem>();
572
+ for (const workflow of workflowList) {
573
+ workflowItemsById.set(
574
+ workflow.id,
575
+ buildWorkflowItem(workflow, workflowRooms.get(workflow.id), {
576
+ workflowId: workflow.id,
577
+ workflowName: workflow.name,
578
+ })
579
+ );
580
+ }
581
+
582
+ for (const trigger of triggerItems) {
583
+ if (trigger.kind === 'workflow' && trigger.workflowId) {
584
+ const existing = workflowItemsById.get(trigger.workflowId);
585
+ if (existing) {
586
+ existing.schedules = [...existing.schedules, trigger];
587
+ existing.updatedAt =
588
+ existing.updatedAt ??
589
+ normalizeDateValue(trigger.updatedAt) ??
590
+ normalizeDateValue(trigger.lastRunAtIso);
591
+ continue;
592
+ }
593
+ workflowItemsById.set(
594
+ trigger.workflowId,
595
+ buildWorkflowItem(undefined, workflowRooms.get(trigger.workflowId), {
596
+ workflowId: trigger.workflowId,
597
+ workflowName: trigger.workflowName,
598
+ trigger,
599
+ })
600
+ );
601
+ }
602
+ }
603
+
604
+ // Only synthesize workflow items from rooms when workflow runtime is offline
605
+ // (`workflowFetchError` set) — in that case the room is the most-recent
606
+ // ground truth we have and should be surfaced. When workflow runtime is online and
607
+ // returned a list, any workflowId in `workflowRooms` that isn't in the
608
+ // current workflow list is an ORPHAN: the workflow was deleted but the chat
609
+ // room/conversation wasn't cleaned up. Surfacing those creates ghost
610
+ // rows the user can't dismiss. Skip them; the UI's deleteWorkflow path
611
+ // also deletes the conversation now, so future deletions won't leak rooms.
612
+ const workflowOffline = workflowFetchError !== null;
613
+ if (workflowOffline) {
614
+ for (const [workflowId, room] of workflowRooms.entries()) {
615
+ if (!workflowItemsById.has(workflowId)) {
616
+ workflowItemsById.set(
617
+ workflowId,
618
+ buildWorkflowItem(undefined, room, {
619
+ workflowId,
620
+ workflowName: room.metadata.workflowName,
621
+ })
622
+ );
623
+ }
624
+ }
625
+ }
626
+
627
+ // Fetch last execution for each live workflow in parallel.
628
+ // Promise.allSettled ensures one failure does not block the full list.
629
+ if (!workflowOffline && service && workflowItemsById.size > 0) {
630
+ const now = Date.now();
631
+ for (const [k, v] of lastExecutionCache) {
632
+ if (v.expiresAt < now) lastExecutionCache.delete(k);
633
+ }
634
+ const workflowIds = [...workflowItemsById.keys()];
635
+ await Promise.allSettled(
636
+ workflowIds.map(async (workflowId) => {
637
+ const exec = await fetchLastExecution(service, workflowId);
638
+ if (!exec) return;
639
+ const item = workflowItemsById.get(workflowId);
640
+ if (item) item.lastExecution = exec;
641
+ })
642
+ );
643
+ }
644
+
645
+ const coordinatorTriggerItems = triggerItems
646
+ .filter((trigger) => trigger.kind !== 'workflow')
647
+ .map((trigger) => buildCoordinatorTriggerItem(trigger, triggerRooms.get(trigger.id)));
648
+
649
+ const automations = [
650
+ ...automationDraftItems,
651
+ ...workflowDraftItems,
652
+ ...taskItems,
653
+ ...coordinatorTriggerItems,
654
+ ...workflowItemsById.values(),
655
+ ].sort(compareAutomationItems);
656
+
657
+ const summary: AutomationSummary = {
658
+ total: automations.length,
659
+ coordinatorCount: automations.filter((automation) => automation.type === 'coordinator_text')
660
+ .length,
661
+ workflowCount: automations.filter((automation) => automation.type === 'workflow').length,
662
+ scheduledCount: automations.filter((automation) => automation.schedules.length > 0).length,
663
+ draftCount: automations.filter((automation) => automation.isDraft).length,
664
+ };
665
+
666
+ return {
667
+ automations,
668
+ summary,
669
+ workflowStatus,
670
+ workflowFetchError,
671
+ };
672
+ }
673
+
674
+ /**
675
+ * Test-only: clear the last-execution cache between cases.
676
+ */
677
+ export function __resetAutomationsCacheForTests(): void {
678
+ lastExecutionCache.clear();
679
+ }