@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,39 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers'
3
+ import { createJobContext, type AutomationJob } from '../utils/automation-context'
4
+ import { closeUnfinishedRun, executeAutomation, type DurableStep } from '../utils/automation-engine'
5
+ import { retryTransientFailures } from '../utils/transient-retry'
6
+
7
+ // Application failures are recorded per agent attempt by the engine, so the
8
+ // platform never replays a step; brief D1 and network interruptions are retried
9
+ // inside it instead.
10
+ const STEP = { retries: { limit: 0, delay: '1 second' }, timeout: '10 minutes' } as const
11
+
12
+ export class AutomationRunner extends WorkflowEntrypoint<Cloudflare.Env, AutomationJob> {
13
+ async run(event: WorkflowEvent<AutomationJob>, step: WorkflowStep) {
14
+ const context = await retryTransientFailures(() =>
15
+ createJobContext(event.payload, this.env as Record<string, unknown>),
16
+ )
17
+ const durable: DurableStep = (name, callback) =>
18
+ step.do(name, STEP, async () =>
19
+ JSON.parse(JSON.stringify((await retryTransientFailures(callback)) ?? null)),
20
+ )
21
+ let wake = 0
22
+ try {
23
+ return await executeAutomation(event.payload, context, durable, async (id) => {
24
+ await step.waitForEvent(`approval ${id} wake ${wake++}`, {
25
+ type: `integration-approval-${id}`,
26
+ timeout: '30 days',
27
+ })
28
+ })
29
+ } catch (cause) {
30
+ // Close the run's record before the instance errors, so history never shows it running.
31
+ await durable('close unfinished run', () =>
32
+ closeUnfinishedRun(event.payload, context, cause),
33
+ ).catch((closeError: unknown) => {
34
+ console.error('[automations] Could not close an interrupted run.', closeError)
35
+ })
36
+ throw cause
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,5 @@
1
+ import type { WorkspaceMetadata } from '@nuxtagentic/prompt/server-contributions'
2
+ export default {
3
+ excludedModels: ['automationRun', 'automationFolder'],
4
+ resourceAliases: { automation: ['automation', 'automations', 'workflow', 'workflows'] },
5
+ } satisfies WorkspaceMetadata
@@ -0,0 +1,61 @@
1
+ import type { AutomationStep } from './automation'
2
+
3
+ export type RunStatus = 'queued' | 'running' | 'waiting' | 'succeeded' | 'failed'
4
+ export type DisplayStatus = RunStatus | 'skipped' | 'retry' | undefined
5
+
6
+ const statusColors: Partial<Record<NonNullable<DisplayStatus>, string>> = {
7
+ succeeded: 'text-success',
8
+ failed: 'text-error',
9
+ retry: 'text-warning',
10
+ running: 'text-highlighted',
11
+ }
12
+
13
+ export function getStatusColor(status: DisplayStatus) {
14
+ return (status && statusColors[status]) || 'text-dimmed'
15
+ }
16
+
17
+ export function getStepStatus(step: AutomationStep | undefined): DisplayStatus {
18
+ return step?.status === 'running' && (step.attempt ?? 0) > 1 ? 'retry' : step?.status
19
+ }
20
+
21
+ export function formatElapsedDuration(
22
+ start: string | null | undefined,
23
+ end: string | null | undefined,
24
+ ) {
25
+ if (!start || !end) {
26
+ return ''
27
+ }
28
+ const seconds = (new Date(end).getTime() - new Date(start).getTime()) / 1000
29
+ return Number.isFinite(seconds) && seconds >= 0 ? `${seconds.toFixed(1)}s` : ''
30
+ }
31
+
32
+ export function formatStepDuration(step: AutomationStep | undefined) {
33
+ if (!step) {
34
+ return ''
35
+ }
36
+ const { status, startedAt, finishedAt, logs, at } = step
37
+ if (status === 'skipped') {
38
+ return ''
39
+ }
40
+ if (status === 'running') {
41
+ return '…'
42
+ }
43
+ if (!startedAt && (logs?.length ?? 0) < 2) {
44
+ return ''
45
+ }
46
+ return formatElapsedDuration(
47
+ startedAt ?? logs?.[0]?.at ?? at,
48
+ finishedAt ?? logs?.at(-1)?.at ?? at,
49
+ )
50
+ }
51
+
52
+ export function getStepLogs(step: AutomationStep | undefined) {
53
+ if (!step) {
54
+ return []
55
+ }
56
+ const { status, logs, at, message } = step
57
+ if (status === 'skipped') {
58
+ return []
59
+ }
60
+ return logs?.length ? logs : [{ at, status, message }]
61
+ }
@@ -0,0 +1,202 @@
1
+ export type AutomationNode = 'trigger' | 'agent' | 'success' | 'error'
2
+ export type AutomationAction = 'none' | 'email' | 'slack' | 'record' | 'webhook' | 'notify'
3
+ export interface AutomationTrigger {
4
+ kind: 'event' | 'webhook' | 'schedule'
5
+ event: string
6
+ slug: string
7
+ cron?: string
8
+ timeZone?: string
9
+ }
10
+ export interface AutomationOutcome {
11
+ action: AutomationAction
12
+ target: string
13
+ }
14
+ export type AutomationPositions = Record<AutomationNode, { x: number; y: number }>
15
+ export interface AutomationDraft {
16
+ name: string
17
+ folder: string
18
+ active: boolean
19
+ workspace: string
20
+ trigger: AutomationTrigger
21
+ agentId: number
22
+ instructions: string
23
+ timeout: number
24
+ retries: number
25
+ success: AutomationOutcome
26
+ failure: AutomationOutcome
27
+ positions: AutomationPositions
28
+ /**
29
+ * Whose connections runs may use: the owner's own plus the workspace's, or
30
+ * only the workspace's shared ones so the automation never acts as a person.
31
+ */
32
+ runAs?: 'owner' | 'workspace'
33
+ }
34
+ export interface AutomationStep {
35
+ node: AutomationNode
36
+ status: 'running' | 'succeeded' | 'failed' | 'skipped'
37
+ at: string
38
+ message: string
39
+ summary?: string
40
+ startedAt?: string
41
+ finishedAt?: string
42
+ attempt?: number
43
+ retryLimit?: number
44
+ details?: Record<string, string>
45
+ logs?: {
46
+ at: string
47
+ status: AutomationStep['status']
48
+ message: string
49
+ level?: 'info' | 'warning' | 'error'
50
+ }[]
51
+ }
52
+ export const AUTOMATION_POSITIONS: AutomationPositions = {
53
+ trigger: { x: -140, y: 40 },
54
+ agent: { x: -140, y: 250 },
55
+ success: { x: -330, y: 480 },
56
+ error: { x: 50, y: 480 },
57
+ }
58
+ export const LOCAL_EVENTS = [
59
+ {
60
+ id: 'user.created',
61
+ label: 'A user is created',
62
+ detail: 'An administrator creates an account.',
63
+ },
64
+ {
65
+ id: 'user.deactivated',
66
+ label: 'A user is deactivated',
67
+ detail: 'An administrator disables an account.',
68
+ },
69
+ { id: 'file.uploaded', label: 'A file is uploaded', detail: 'A local file finishes uploading.' },
70
+ {
71
+ id: 'task.moved',
72
+ label: 'A task changes status',
73
+ detail: 'A board card moves to another status.',
74
+ },
75
+ ] as const
76
+ export const SUCCESS_ACTIONS = [
77
+ {
78
+ value: 'email',
79
+ label: 'Send email',
80
+ icon: 'i-lucide-mail',
81
+ detail: 'Send the result through your Gmail connection.',
82
+ },
83
+ {
84
+ value: 'slack',
85
+ label: 'Post to Slack',
86
+ icon: 'i-lucide-hash',
87
+ detail: 'Post the result to a connected channel.',
88
+ },
89
+ {
90
+ value: 'record',
91
+ label: 'Update record',
92
+ icon: 'i-lucide-database',
93
+ detail: 'Write the result to a field on the triggering record.',
94
+ },
95
+ {
96
+ value: 'webhook',
97
+ label: 'Call webhook',
98
+ icon: 'i-lucide-webhook',
99
+ detail: 'POST the result to an allowed HTTPS endpoint.',
100
+ },
101
+ {
102
+ value: 'none',
103
+ label: 'Keep result',
104
+ icon: 'i-lucide-check',
105
+ detail: 'Save the result in run history.',
106
+ },
107
+ ] as const
108
+ export const ERROR_ACTIONS = [
109
+ {
110
+ value: 'notify',
111
+ label: 'Notify owner',
112
+ icon: 'i-lucide-mail',
113
+ detail: 'Email a failure report through Gmail.',
114
+ },
115
+ {
116
+ value: 'slack',
117
+ label: 'Post to Slack',
118
+ icon: 'i-lucide-hash',
119
+ detail: 'Post a failure alert to a connected channel.',
120
+ },
121
+ {
122
+ value: 'none',
123
+ label: 'Stop silently',
124
+ icon: 'i-lucide-circle-stop',
125
+ detail: 'Record the failure without sending a notification.',
126
+ },
127
+ ] as const
128
+ export function createAutomationDraft(workspace: string, folder = 'Unsorted'): AutomationDraft {
129
+ return {
130
+ name: 'Untitled automation',
131
+ folder,
132
+ active: false,
133
+ workspace,
134
+ trigger: { kind: 'event', event: 'user.created', slug: 'new-signup' },
135
+ agentId: 0,
136
+ instructions: 'Use the event payload to complete your task. Return a concise result.',
137
+ timeout: 30,
138
+ retries: 2,
139
+ success: { action: 'none', target: '' },
140
+ failure: { action: 'none', target: '' },
141
+ positions: structuredClone(AUTOMATION_POSITIONS),
142
+ runAs: 'owner',
143
+ }
144
+ }
145
+ export function getTargetLabel(action: AutomationAction) {
146
+ return {
147
+ none: '',
148
+ email: 'Recipient',
149
+ notify: 'Owner email',
150
+ slack: 'Channel',
151
+ record: 'Field',
152
+ webhook: 'URL',
153
+ }[action]
154
+ }
155
+
156
+ /** The short label for what starts an automation, such as its webhook route or cron schedule. */
157
+ export function describeTrigger(trigger: AutomationTrigger) {
158
+ switch (trigger.kind) {
159
+ case 'webhook':
160
+ return `POST /hooks/${trigger.slug}`
161
+ case 'schedule':
162
+ return `${trigger.cron ?? ''} · ${trigger.timeZone ?? ''}`
163
+ default:
164
+ return trigger.event
165
+ }
166
+ }
167
+
168
+ /** Labels shared by the workspace canvas and embedded examples. */
169
+ export function describeAutomationNodes(
170
+ draft: AutomationDraft,
171
+ context: { agentName?: string; eventLabel?: string },
172
+ ): Record<AutomationNode, { main: string; sub: string }> {
173
+ let triggerSource = context.eventLabel ?? 'Connected application event'
174
+ if (draft.trigger.kind === 'webhook') {
175
+ triggerSource = 'Authenticated webhook'
176
+ } else if (draft.trigger.kind === 'schedule') {
177
+ triggerSource = 'Scheduled run'
178
+ }
179
+
180
+ const successLabel =
181
+ SUCCESS_ACTIONS.find((action) => action.value === draft.success.action)?.label ?? 'Keep result'
182
+ const failureLabel =
183
+ ERROR_ACTIONS.find((action) => action.value === draft.failure.action)?.label ?? 'Stop silently'
184
+ const errorSummary =
185
+ draft.retries > 0 ? `Retry ${draft.retries}×, then ${failureLabel.toLowerCase()}` : failureLabel
186
+
187
+ return {
188
+ trigger: { main: describeTrigger(draft.trigger), sub: triggerSource },
189
+ agent: {
190
+ main: context.agentName ?? 'Choose an agent',
191
+ sub: context.agentName ? draft.instructions : 'Select a saved agent and instructions',
192
+ },
193
+ success: {
194
+ main: successLabel,
195
+ sub: draft.success.target || 'Save the agent result in run history',
196
+ },
197
+ error: {
198
+ main: errorSummary,
199
+ sub: draft.failure.target || 'Failure is logged in run history',
200
+ },
201
+ }
202
+ }
@@ -0,0 +1,56 @@
1
+ import { computed } from 'vue'
2
+ import type { WorkspaceContribution } from '@nuxtagentic/workspace/runtime/types'
3
+ import type { AgentAction } from '@nuxtagentic/workspace/runtime/shared/agent-actions'
4
+
5
+ const commands = [
6
+ ['save-automation', 'Save automation', 'Save this automation.', 'i-lucide-save'],
7
+ ['test-automation', 'Test run', 'Run this automation with a sample event.', 'i-lucide-play'],
8
+ [
9
+ 'webhook-automation',
10
+ 'Trigger on a webhook',
11
+ 'Use an authenticated webhook trigger.',
12
+ 'i-lucide-webhook',
13
+ ],
14
+ [
15
+ 'retry-automation',
16
+ 'Retry 3 times on error',
17
+ 'Set agent retries to three.',
18
+ 'i-lucide-rotate-cw',
19
+ ],
20
+ [
21
+ 'slack-automation',
22
+ 'Post errors to Slack',
23
+ 'Choose a Slack failure notification.',
24
+ 'i-lucide-hash',
25
+ ],
26
+ ] as const
27
+ const actions: AgentAction[] = commands.map(([id, name, description, icon]) => ({
28
+ id,
29
+ name,
30
+ description,
31
+ icon,
32
+ available: (context) => context.canRun(id),
33
+ run: (context) => context.run(id),
34
+ }))
35
+ export const automationsContribution = {
36
+ id: 'automations',
37
+ label: 'Automations',
38
+ icon: 'i-lucide-workflow',
39
+ selectionKey: 'automation',
40
+ matchesRoute: (route) => route.path === '/dashboard/automations',
41
+ navigation: { order: 70, group: 'workspace' },
42
+ navigationShortcuts: [
43
+ { label: 'Automations', icon: 'i-lucide-workflow', to: '/dashboard/automations' },
44
+ ],
45
+ layout: {
46
+ navbar: true,
47
+ hideTitle: true,
48
+ navbarClass: 'gap-2.5 bg-(--gray-200) px-3 ps-2.5 sm:px-3 sm:ps-2.5',
49
+ edgeToEdge: true,
50
+ flat: true,
51
+ hideExpand: true,
52
+ ownsDetailAside: true,
53
+ },
54
+ usePromptPresentation: () => computed(() => ({ placeholder: 'Describe your automation…' })),
55
+ actions: { automations: actions },
56
+ } satisfies WorkspaceContribution
package/schema.ts ADDED
@@ -0,0 +1,222 @@
1
+ import { model, t } from '@cloudconfig/core'
2
+ import { createAutomationDraft } from './runtime/shared/automation'
3
+ import { nextAgentSchedule } from '@cloudconfig/core/agents/schedules'
4
+ import { syncAutomationSchedule } from './runtime/server/utils/schedule-sync'
5
+ const defaults = createAutomationDraft('personal')
6
+
7
+ export const automationsModels = {
8
+ automation: model(
9
+ {
10
+ name: t.text().required(),
11
+ folder: t.text().default('Unsorted'),
12
+ active: t.boolean().default(false),
13
+ workspace: t.text().required(),
14
+ trigger: t.json().default(defaults.trigger),
15
+ agentId: t.number().default(0),
16
+ instructions: t.textarea().default(defaults.instructions),
17
+ timeout: t.number().default(30),
18
+ retries: t.number().default(2),
19
+ success: t.json().default(defaults.success),
20
+ failure: t.json().default(defaults.failure),
21
+ positions: t.json().default(defaults.positions),
22
+ runAs: t.select(['owner', 'workspace']).default('owner'),
23
+ webhookToken: t.text().required().serverOnly(),
24
+ ownerId: t.number().required().serverOnly(),
25
+ },
26
+ {
27
+ access: {
28
+ list: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
29
+ read: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
30
+ create: ({ user }) => !!user,
31
+ update: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
32
+ delete: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
33
+ },
34
+ hooks: {
35
+ beforeCreate: ({ user }, input) => ({
36
+ ...input,
37
+ ownerId: user!.id,
38
+ webhookToken: crypto.randomUUID() + crypto.randomUUID(),
39
+ }),
40
+ beforeUpdate: ({ user }, input) => ({ ...input, ownerId: user!.id }),
41
+ afterChange: async ({ user, env }, change) => {
42
+ if (user) {
43
+ await syncAutomationSchedule(env, Number(user.id), Number(change.id), change.row)
44
+ }
45
+ },
46
+ },
47
+ validate: (input, existing) => {
48
+ const issues: { path: string; message: string }[] = []
49
+ if (!String(input.name ?? '').trim() || String(input.name).length > 200) {
50
+ issues.push({
51
+ path: 'name',
52
+ message: 'Use an automation name between 1 and 200 characters.',
53
+ })
54
+ }
55
+ if (!String(input.folder ?? '').trim() || String(input.folder).length > 80) {
56
+ issues.push({ path: 'folder', message: 'Use a folder name between 1 and 80 characters.' })
57
+ }
58
+ if (!input.workspace || (existing && input.workspace !== existing.workspace)) {
59
+ issues.push({
60
+ path: 'workspace',
61
+ message: 'Keep this automation in its original workspace.',
62
+ })
63
+ }
64
+ const trigger = input.trigger as Record<string, unknown> | null
65
+ if (
66
+ !trigger ||
67
+ !['event', 'webhook', 'schedule'].includes(String(trigger.kind)) ||
68
+ (trigger.kind === 'event' && typeof trigger.event !== 'string') ||
69
+ (trigger.kind === 'webhook' && !/^[a-z0-9-]{1,80}$/.test(String(trigger.slug)))
70
+ ) {
71
+ issues.push({
72
+ path: 'trigger',
73
+ message:
74
+ 'Choose an event, schedule, or webhook path using lowercase letters, numbers and hyphens.',
75
+ })
76
+ }
77
+ if (trigger?.kind === 'schedule') {
78
+ try {
79
+ nextAgentSchedule(String(trigger.cron ?? ''), String(trigger.timeZone ?? ''))
80
+ } catch {
81
+ issues.push({
82
+ path: 'trigger',
83
+ message:
84
+ 'Use a five-field cron expression and an IANA time zone, such as Europe/Amsterdam.',
85
+ })
86
+ }
87
+ }
88
+ if (![15, 30, 60].includes(Number(input.timeout))) {
89
+ issues.push({ path: 'timeout', message: 'Choose a timeout of 15, 30 or 60 seconds.' })
90
+ }
91
+ if (
92
+ !Number.isInteger(input.retries) ||
93
+ Number(input.retries) < 0 ||
94
+ Number(input.retries) > 5
95
+ ) {
96
+ issues.push({ path: 'retries', message: 'Choose between 0 and 5 retries.' })
97
+ }
98
+ if (
99
+ !Number.isSafeInteger(input.agentId) ||
100
+ Number(input.agentId) < 0 ||
101
+ (input.active && !input.agentId)
102
+ ) {
103
+ issues.push({
104
+ path: 'agentId',
105
+ message: 'Choose an agent before activating this automation.',
106
+ })
107
+ }
108
+ if (String(input.instructions).length > 20000) {
109
+ issues.push({
110
+ path: 'instructions',
111
+ message: 'Use instructions of 20,000 characters or fewer.',
112
+ })
113
+ }
114
+ for (const [field, actions] of [
115
+ ['success', ['none', 'email', 'slack', 'record', 'webhook']],
116
+ ['failure', ['none', 'notify', 'slack']],
117
+ ] as const) {
118
+ const outcome = input[field] as Record<string, unknown> | null
119
+ if (
120
+ !outcome ||
121
+ !actions.includes(String(outcome.action) as never) ||
122
+ typeof outcome.target !== 'string' ||
123
+ outcome.target.length > 2048 ||
124
+ (outcome.action !== 'none' && !outcome.target.trim())
125
+ ) {
126
+ issues.push({ path: field, message: 'Choose an action and enter its destination.' })
127
+ }
128
+ }
129
+ const positions = input.positions as Record<string, { x: number; y: number }> | null
130
+ if (
131
+ !positions ||
132
+ ['trigger', 'agent', 'success', 'error'].some(
133
+ (node) => !Number.isFinite(positions[node]?.x) || !Number.isFinite(positions[node]?.y),
134
+ )
135
+ ) {
136
+ issues.push({ path: 'positions', message: 'Use valid node positions.' })
137
+ }
138
+ return issues
139
+ },
140
+ },
141
+ ),
142
+ automationFolder: model(
143
+ {
144
+ name: t.text().required(),
145
+ workspace: t.text().required(),
146
+ ownerId: t.number().required().serverOnly(),
147
+ },
148
+ {
149
+ access: {
150
+ list: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
151
+ read: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
152
+ create: ({ user }) => !!user,
153
+ delete: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
154
+ },
155
+ hooks: {
156
+ beforeCreate: ({ user }, input) => ({
157
+ ...input,
158
+ name: String(input.name ?? '').trim(),
159
+ ownerId: user!.id,
160
+ }),
161
+ },
162
+ validate: (input) => {
163
+ const name = String(input.name ?? '')
164
+ if (!name || name.length > 80) {
165
+ return [{ path: 'name', message: 'Use a folder name between 1 and 80 characters.' }]
166
+ }
167
+ if (name.toLowerCase() === 'unsorted') {
168
+ return [{ path: 'name', message: 'Choose a different folder name.' }]
169
+ }
170
+ return []
171
+ },
172
+ },
173
+ ),
174
+ automationRun: model(
175
+ {
176
+ automationId: t.number().required(),
177
+ workspace: t.text().required(),
178
+ eventId: t.text().required().unique(),
179
+ status: t.select(['queued', 'running', 'waiting', 'succeeded', 'failed']).default('queued'),
180
+ payload: t.json().required(),
181
+ steps: t.json().default([]),
182
+ output: t.textarea().default(''),
183
+ error: t.textarea().default(''),
184
+ attempts: t.number().default(0),
185
+ startedAt: t.datetime(),
186
+ finishedAt: t.datetime(),
187
+ snapshot: t.json().required().serverOnly(),
188
+ journal: t.json().default({}).serverOnly(),
189
+ ownerId: t.number().required().serverOnly(),
190
+ },
191
+ {
192
+ access: {
193
+ list: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
194
+ read: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
195
+ create: ({ user, input }) => !!user && input?.ownerId === user.id,
196
+ update: ({ user, input, where }) =>
197
+ user && input?.ownerId === user.id ? where('ownerId', 'eq', user.id) : false,
198
+ },
199
+ },
200
+ ),
201
+ // One row per journaled model or tool call, so a long run never outgrows D1's row limit.
202
+ automationRunReceipt: model(
203
+ {
204
+ runId: t.number().required(),
205
+ key: t.text().required().unique(),
206
+ status: t.select(['started', 'completed']).default('started').serverOnly(),
207
+ value: t.json().serverOnly(),
208
+ ownerId: t.number().required().serverOnly(),
209
+ },
210
+ {
211
+ // Writes require the server-only owner, so only the automation engine records receipts.
212
+ access: {
213
+ list: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
214
+ read: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
215
+ create: ({ user, input }) => !!user && input?.ownerId === user.id,
216
+ update: ({ user, input, where }) =>
217
+ user && input?.ownerId === user.id ? where('ownerId', 'eq', user.id) : false,
218
+ },
219
+ },
220
+ ),
221
+ }
222
+ export type AutomationsConfig = { models: typeof automationsModels }
package/server.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Scheduled automation dispatch for a CloudConfig agent host. */
2
+ export { fireAutomationSchedule } from './runtime/server/utils/automation-schedule'
package/workflow.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Cloudflare Workflow entry point. Import only from a Worker entry adapter. */
2
+ export { AutomationRunner } from './runtime/server/workflows/runner'