@nuxtagentic/automations 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +173 -2
  3. package/client.ts +24 -0
  4. package/module.ts +55 -0
  5. package/package.json +59 -3
  6. package/runtime/components/AutomationCanvas.vue +488 -0
  7. package/runtime/components/AutomationInspector.vue +436 -0
  8. package/runtime/components/AutomationNodeIcon.vue +27 -0
  9. package/runtime/components/AutomationRunsPanel.vue +475 -0
  10. package/runtime/components/AutomationSidebar.vue +253 -0
  11. package/runtime/components/AutomationStatusIcon.vue +44 -0
  12. package/runtime/pages/automations.vue +1181 -0
  13. package/runtime/server/api/automations/[id]/endpoint.get.ts +12 -0
  14. package/runtime/server/api/automations/[id]/run.post.ts +30 -0
  15. package/runtime/server/api/automations/events.post.ts +37 -0
  16. package/runtime/server/api/automations/runs/[id]/resume.post.ts +23 -0
  17. package/runtime/server/handlers/automation-hook.post.ts +51 -0
  18. package/runtime/server/handlers/connect-hook.post.ts +54 -0
  19. package/runtime/server/handlers/integration-hook.post.ts +86 -0
  20. package/runtime/server/plugins/events.ts +79 -0
  21. package/runtime/server/plugins/maintenance.ts +20 -0
  22. package/runtime/server/utils/agent-runner.ts +15 -0
  23. package/runtime/server/utils/automation-context.ts +77 -0
  24. package/runtime/server/utils/automation-engine.ts +697 -0
  25. package/runtime/server/utils/automation-launch.ts +205 -0
  26. package/runtime/server/utils/automation-schedule.ts +31 -0
  27. package/runtime/server/utils/connection-events.ts +114 -0
  28. package/runtime/server/utils/maintenance.ts +255 -0
  29. package/runtime/server/utils/schedule-sync.ts +32 -0
  30. package/runtime/server/utils/transient-retry.ts +34 -0
  31. package/runtime/server/workflows/runner.ts +39 -0
  32. package/runtime/shared/agent.ts +5 -0
  33. package/runtime/shared/automation-run.ts +61 -0
  34. package/runtime/shared/automation.ts +202 -0
  35. package/runtime/workspace.ts +56 -0
  36. package/schema.ts +222 -0
  37. package/server.ts +2 -0
  38. package/workflow.ts +2 -0
@@ -0,0 +1,436 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import type { AgentSummary } from '@nuxtagentic/agents/runtime/shared/agent-summary'
4
+ import {
5
+ SUCCESS_ACTIONS,
6
+ ERROR_ACTIONS,
7
+ getTargetLabel,
8
+ type AutomationDraft,
9
+ type AutomationNode,
10
+ } from '../shared/automation'
11
+ import AutomationNodeIcon from './AutomationNodeIcon.vue'
12
+
13
+ const draft = defineModel<AutomationDraft>({ required: true })
14
+ const {
15
+ node,
16
+ agents,
17
+ events,
18
+ connectedProviders,
19
+ agentsInstalled = true,
20
+ agentError = '',
21
+ endpoint = '',
22
+ sample = '',
23
+ copied = false,
24
+ } = defineProps<{
25
+ node: AutomationNode
26
+ agents: AgentSummary[]
27
+ events: { id: string; label: string; detail: string }[]
28
+ connectedProviders: string[]
29
+ agentsInstalled?: boolean
30
+ agentError?: string
31
+ endpoint?: string
32
+ sample?: string
33
+ copied?: boolean
34
+ }>()
35
+ defineEmits<{ close: []; copy: [] }>()
36
+
37
+ const isConnected = (provider: string) => connectedProviders.includes(provider)
38
+ const options = computed(() => {
39
+ switch (node) {
40
+ case 'trigger':
41
+ return events.map((event) => ({
42
+ value: event.id,
43
+ label: event.id,
44
+ detail: event.label,
45
+ }))
46
+ case 'agent':
47
+ return agents.map((agent) => ({
48
+ value: agent.id,
49
+ label: agent.name,
50
+ detail: `${agent.provider} · ${agent.model}${agent.status === 'draft' ? ' · draft, test only' : ''}`,
51
+ }))
52
+ case 'success':
53
+ return SUCCESS_ACTIONS
54
+ default:
55
+ return ERROR_ACTIONS
56
+ }
57
+ })
58
+ const outcome = computed(() => (node === 'success' ? draft.value.success : draft.value.failure))
59
+ const panel = computed(
60
+ () =>
61
+ ({
62
+ trigger: { label: 'Trigger', color: 'text-warning', optionLabel: 'Event' },
63
+ agent: { label: 'Agent call', color: 'text-info', optionLabel: 'Agent' },
64
+ success: { label: 'On success', color: 'text-success', optionLabel: 'Action' },
65
+ error: { label: 'On error', color: 'text-error', optionLabel: 'Then' },
66
+ })[node],
67
+ )
68
+ const targetPlaceholder = computed(() => {
69
+ switch (outcome.value.action) {
70
+ case 'record':
71
+ return 'description'
72
+ case 'slack':
73
+ return 'C0123456789'
74
+ case 'webhook':
75
+ return 'https://example.com/agent-result'
76
+ default:
77
+ return 'user.email'
78
+ }
79
+ })
80
+
81
+ function pick(value: string | number) {
82
+ if (node === 'trigger') {
83
+ draft.value.trigger.event = String(value)
84
+ } else if (node === 'agent') {
85
+ draft.value.agentId = Number(value)
86
+ } else {
87
+ const action = [...SUCCESS_ACTIONS, ...ERROR_ACTIONS].find((option) => option.value === value)
88
+ if (action) {
89
+ outcome.value.action = action.value
90
+ }
91
+ }
92
+ }
93
+ function isPicked(value: string | number) {
94
+ switch (node) {
95
+ case 'trigger':
96
+ return draft.value.trigger.event === value
97
+ case 'agent':
98
+ return draft.value.agentId === value
99
+ default:
100
+ return outcome.value.action === value
101
+ }
102
+ }
103
+ </script>
104
+
105
+ <template>
106
+ <div class="flex h-full min-h-0 flex-col">
107
+ <div class="flex h-13 shrink-0 items-center gap-2.5 border-b border-(--gray-400) pr-2 pl-4">
108
+ <span
109
+ class="grid size-6 place-items-center rounded-[7px] bg-(--gray-500)"
110
+ :class="panel.color"
111
+ >
112
+ <AutomationNodeIcon
113
+ :node="node"
114
+ class="size-3.5"
115
+ />
116
+ </span>
117
+ <span class="flex-1 text-[15px]">{{ panel.label }}</span>
118
+ <UButton
119
+ icon="i-lucide-x"
120
+ aria-label="Close inspector"
121
+ color="neutral"
122
+ variant="ghost"
123
+ class="size-8 justify-center rounded-md p-0 text-muted hover:bg-(--gray-400) hover:text-highlighted"
124
+ :ui="{ leadingIcon: 'size-4' }"
125
+ @click="$emit('close')"
126
+ />
127
+ </div>
128
+ <div class="flex min-h-0 flex-1 flex-col gap-5 overflow-x-hidden overflow-y-auto p-4">
129
+ <div
130
+ v-if="node === 'trigger'"
131
+ class="flex flex-col gap-2"
132
+ >
133
+ <span class="text-xs text-dimmed">Fires on</span>
134
+ <UTabs
135
+ v-model="draft.trigger.kind"
136
+ :items="[
137
+ { label: 'Event', value: 'event' },
138
+ { label: 'Webhook', value: 'webhook' },
139
+ { label: 'Schedule', value: 'schedule' },
140
+ ]"
141
+ :content="false"
142
+ color="neutral"
143
+ :ui="{
144
+ list: 'h-[34px] rounded-[9px] border border-(--gray-400) bg-(--gray-100) p-0.5',
145
+ trigger: 'text-[13px] font-normal data-[state=active]:text-highlighted',
146
+ indicator: 'rounded-[7px] bg-(--gray-500) ring-0',
147
+ }"
148
+ />
149
+ </div>
150
+ <template v-if="node === 'trigger' && draft.trigger.kind === 'webhook'">
151
+ <div class="flex flex-col gap-2">
152
+ <span class="text-xs text-dimmed">Endpoint</span>
153
+ <div
154
+ class="flex h-9 items-center gap-1.5 rounded-lg border border-(--gray-400) bg-(--gray-100) pr-1 pl-2.5"
155
+ >
156
+ <span class="font-mono text-[11px] text-warning">POST</span>
157
+ <span class="min-w-0 flex-1 truncate font-mono text-xs">
158
+ {{ endpoint || 'Save to generate endpoint' }}
159
+ </span>
160
+ <UButton
161
+ :label="copied ? 'Copied' : 'Copy'"
162
+ color="neutral"
163
+ variant="soft"
164
+ size="xs"
165
+ class="h-7 rounded-md"
166
+ :disabled="!endpoint"
167
+ @click="$emit('copy')"
168
+ />
169
+ </div>
170
+ </div>
171
+ <UFormField
172
+ label="Path"
173
+ :ui="{ label: 'text-xs font-normal text-dimmed' }"
174
+ >
175
+ <UInput
176
+ v-model="draft.trigger.slug"
177
+ class="w-full"
178
+ :ui="{
179
+ base: 'h-9 rounded-lg bg-(--gray-100) px-2.5 font-mono text-[13px] ring-(--gray-400)',
180
+ }"
181
+ />
182
+ </UFormField>
183
+ <div class="flex flex-col gap-2">
184
+ <span class="text-xs text-dimmed">Sample payload</span>
185
+ <pre
186
+ class="rounded-lg border border-(--gray-400) bg-(--gray-100) p-3 font-mono text-xs leading-[18px] whitespace-pre-wrap text-toned"
187
+ >{{ sample }}</pre>
188
+ <span class="text-xs leading-4 text-dimmed">
189
+ Send a unique Idempotency-Key header with each event.
190
+ </span>
191
+ </div>
192
+ </template>
193
+ <div
194
+ v-else-if="node === 'trigger' && draft.trigger.kind === 'schedule'"
195
+ class="flex flex-col gap-3"
196
+ >
197
+ <UFormField
198
+ label="Cron expression"
199
+ help="Five fields: minute, hour, day, month, weekday."
200
+ >
201
+ <UInput
202
+ v-model="draft.trigger.cron"
203
+ placeholder="0 9 * * 1-5"
204
+ class="w-full"
205
+ />
206
+ </UFormField>
207
+ <UFormField
208
+ label="Time zone"
209
+ help="Schedules follow this zone, including daylight saving time."
210
+ >
211
+ <UInput
212
+ v-model="draft.trigger.timeZone"
213
+ placeholder="Europe/Amsterdam"
214
+ class="w-full"
215
+ />
216
+ </UFormField>
217
+ <p class="text-xs text-muted">
218
+ Activate and save to schedule the next run. Pausing cancels future runs.
219
+ </p>
220
+ </div>
221
+ <div
222
+ v-else
223
+ class="flex flex-col gap-2"
224
+ >
225
+ <span class="text-xs text-dimmed">
226
+ {{ panel.optionLabel }}
227
+ </span>
228
+ <div class="flex flex-col gap-0.5">
229
+ <button
230
+ v-for="option in options"
231
+ :key="option.value"
232
+ type="button"
233
+ class="flex min-h-12 cursor-pointer items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-left transition-colors focus-visible:outline-2 focus-visible:outline-primary motion-reduce:transition-none"
234
+ :class="isPicked(option.value) ? 'bg-(--gray-400)' : 'hover:bg-(--gray-400)'"
235
+ :aria-pressed="isPicked(option.value)"
236
+ @click="pick(option.value)"
237
+ >
238
+ <span class="flex min-w-0 flex-1 flex-col gap-0.5">
239
+ <span
240
+ class="text-[13px]"
241
+ :class="[node === 'trigger' && 'font-mono', 'text-highlighted']"
242
+ >
243
+ {{ option.label }}
244
+ </span>
245
+ <span class="text-xs text-muted">{{ option.detail }}</span>
246
+ </span>
247
+ <svg
248
+ viewBox="0 0 24 24"
249
+ fill="none"
250
+ stroke="currentColor"
251
+ stroke-width="2"
252
+ stroke-linecap="round"
253
+ stroke-linejoin="round"
254
+ aria-hidden="true"
255
+ class="size-4 shrink-0 text-highlighted transition-[opacity,transform] duration-200 motion-reduce:transition-none"
256
+ :class="isPicked(option.value) ? 'scale-100 opacity-100' : 'scale-80 opacity-0'"
257
+ >
258
+ <path d="M20 6 9 17l-5-5" />
259
+ </svg>
260
+ </button>
261
+ </div>
262
+ <UButton
263
+ v-if="node === 'agent' && (!agents.length || !agentsInstalled)"
264
+ :to="agentsInstalled ? '/dashboard/agents' : '/dashboard/modules?search=Agents'"
265
+ :label="agentsInstalled ? 'Create an agent' : 'Install Agents'"
266
+ color="neutral"
267
+ variant="outline"
268
+ icon="i-lucide-bot"
269
+ />
270
+ <p
271
+ v-if="node === 'agent' && agentError"
272
+ class="text-xs text-error"
273
+ >
274
+ {{ agentError }}
275
+ </p>
276
+ </div>
277
+ <template v-if="node === 'agent'">
278
+ <UFormField
279
+ label="Instructions"
280
+ :ui="{ label: 'text-xs font-normal text-dimmed' }"
281
+ >
282
+ <UTextarea
283
+ v-model="draft.instructions"
284
+ :rows="5"
285
+ class="w-full"
286
+ :ui="{
287
+ base: 'rounded-lg bg-(--gray-100) p-2.5 text-[13px] leading-5 ring-(--gray-400)',
288
+ }"
289
+ />
290
+ </UFormField>
291
+ <div class="flex flex-col gap-2">
292
+ <span class="text-xs text-dimmed">Timeout</span>
293
+ <div class="flex gap-1.5">
294
+ <button
295
+ v-for="timeout in [15, 30, 60]"
296
+ :key="timeout"
297
+ type="button"
298
+ class="h-[30px] rounded-full border px-3 text-xs transition-colors duration-140 focus-visible:outline-2 focus-visible:outline-primary motion-reduce:transition-none"
299
+ :class="
300
+ draft.timeout === timeout
301
+ ? 'border-(--gray-600) bg-(--gray-500) text-highlighted'
302
+ : 'border-(--gray-500) text-muted hover:bg-(--gray-400)'
303
+ "
304
+ :aria-pressed="draft.timeout === timeout"
305
+ @click="draft.timeout = timeout"
306
+ >
307
+ {{ timeout }}s
308
+ </button>
309
+ </div>
310
+ </div>
311
+ <div class="flex flex-col gap-2">
312
+ <span class="text-xs text-dimmed">Acts as</span>
313
+ <div class="flex gap-1.5">
314
+ <button
315
+ v-for="option in [
316
+ { value: 'owner', label: 'Me' },
317
+ { value: 'workspace', label: 'Workspace only' },
318
+ ] as const"
319
+ :key="option.value"
320
+ type="button"
321
+ class="h-[30px] rounded-full border px-3 text-xs transition-colors duration-140 focus-visible:outline-2 focus-visible:outline-primary motion-reduce:transition-none"
322
+ :class="
323
+ (draft.runAs ?? 'owner') === option.value
324
+ ? 'border-(--gray-600) bg-(--gray-500) text-highlighted'
325
+ : 'border-(--gray-500) text-muted hover:bg-(--gray-400)'
326
+ "
327
+ :aria-pressed="(draft.runAs ?? 'owner') === option.value"
328
+ @click="draft.runAs = option.value"
329
+ >
330
+ {{ option.label }}
331
+ </button>
332
+ </div>
333
+ <span class="text-xs leading-4 text-dimmed">
334
+ {{
335
+ draft.runAs === 'workspace'
336
+ ? 'Uses only the workspace’s shared connections, never your own accounts.'
337
+ : 'Uses your connections and the workspace’s shared ones.'
338
+ }}
339
+ </span>
340
+ </div>
341
+ </template>
342
+ <div
343
+ v-if="node === 'error'"
344
+ class="flex items-center gap-3"
345
+ >
346
+ <span class="flex min-w-0 flex-1 flex-col gap-0.5">
347
+ <span class="text-[13px] text-highlighted">Retries</span>
348
+ <span class="text-xs text-muted">Call the agent again before handling</span>
349
+ </span>
350
+ <div
351
+ class="flex h-8 shrink-0 items-center rounded-lg border border-(--gray-400) bg-(--gray-100)"
352
+ >
353
+ <UButton
354
+ icon="i-lucide-minus"
355
+ aria-label="Fewer retries"
356
+ color="neutral"
357
+ variant="ghost"
358
+ class="size-7.5 justify-center rounded-[7px] p-0 text-muted"
359
+ :ui="{ leadingIcon: 'size-3.5' }"
360
+ :disabled="draft.retries === 0"
361
+ @click="draft.retries--"
362
+ />
363
+ <span class="min-w-5 text-center text-[13px] tabular-nums text-highlighted">
364
+ {{ draft.retries }}
365
+ </span>
366
+ <UButton
367
+ icon="i-lucide-plus"
368
+ aria-label="More retries"
369
+ color="neutral"
370
+ variant="ghost"
371
+ class="size-7.5 justify-center rounded-[7px] p-0 text-muted"
372
+ :ui="{ leadingIcon: 'size-3.5' }"
373
+ :disabled="draft.retries === 5"
374
+ @click="draft.retries++"
375
+ />
376
+ </div>
377
+ </div>
378
+ <template v-if="(node === 'success' || node === 'error') && outcome.action !== 'none'">
379
+ <UFormField
380
+ :label="getTargetLabel(outcome.action)"
381
+ :ui="{ label: 'text-xs font-normal text-dimmed' }"
382
+ >
383
+ <UInput
384
+ v-model="outcome.target"
385
+ :placeholder="targetPlaceholder"
386
+ class="w-full"
387
+ :ui="{
388
+ base: 'h-9 rounded-lg bg-(--gray-100) px-2.5 font-mono text-[13px] ring-(--gray-400)',
389
+ }"
390
+ />
391
+ </UFormField>
392
+ <p
393
+ v-if="
394
+ (outcome.action === 'email' || outcome.action === 'notify') && !isConnected('gmail')
395
+ "
396
+ class="text-xs leading-4 text-muted"
397
+ >
398
+ Connect Gmail and allow Send email in
399
+ <NuxtLink
400
+ to="/dashboard/integrations?integration=gmail"
401
+ class="underline"
402
+ >
403
+ Integrations
404
+ </NuxtLink>
405
+ .
406
+ </p>
407
+ <p
408
+ v-if="outcome.action === 'slack' && !isConnected('slack')"
409
+ class="text-xs leading-4 text-muted"
410
+ >
411
+ Connect Slack and allow Post messages in
412
+ <NuxtLink
413
+ to="/dashboard/integrations?integration=slack"
414
+ class="underline"
415
+ >
416
+ Integrations
417
+ </NuxtLink>
418
+ .
419
+ </p>
420
+ <p
421
+ v-if="outcome.action === 'record'"
422
+ class="text-xs leading-4 text-muted"
423
+ >
424
+ Enter a writable field on the triggering record. Webhook payloads need record: { model, id
425
+ }.
426
+ </p>
427
+ <p
428
+ v-if="outcome.action === 'webhook'"
429
+ class="text-xs leading-4 text-muted"
430
+ >
431
+ Add the destination’s HTTPS origin to NUXT_INTEGRATIONS_WEBHOOK_ORIGINS on the server.
432
+ </p>
433
+ </template>
434
+ </div>
435
+ </div>
436
+ </template>
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import type { AutomationNode } from '../shared/automation'
3
+
4
+ const { node } = defineProps<{ node: AutomationNode }>()
5
+ const paths: Record<AutomationNode, string> = {
6
+ trigger: 'M13 2 3 14h9l-1 8 10-12h-9l1-8Z',
7
+ agent:
8
+ 'M12 8V4H8M6 8h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2ZM2 14h2M20 14h2M15 13v2M9 13v2',
9
+ success: 'M20 6 9 17l-5-5',
10
+ error:
11
+ 'M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z',
12
+ }
13
+ </script>
14
+
15
+ <template>
16
+ <svg
17
+ viewBox="0 0 24 24"
18
+ fill="none"
19
+ stroke="currentColor"
20
+ stroke-width="1.75"
21
+ stroke-linecap="round"
22
+ stroke-linejoin="round"
23
+ aria-hidden="true"
24
+ >
25
+ <path :d="paths[node]" />
26
+ </svg>
27
+ </template>