Smart Contract

AgentScheduler

A.91d0a5b7c9832a8b.AgentScheduler

Valid From

143,525,904

Deployed

1d ago
Feb 27, 2026, 12:09:04 AM UTC

Dependents

0 imports
1// AgentScheduler.cdc
2// Replaces OpenClaw's unreliable off-chain cron with Flow's native scheduled transactions.
3//
4// WHY THIS IS BETTER:
5// In OpenClaw/ZeroClaw, cron jobs run via Node.js setInterval or system crontab.
6// If the process crashes, the machine sleeps, or the daemon restarts — jobs get missed.
7// Flow's scheduled transactions are executed by VALIDATORS at the protocol level.
8// Once scheduled, they run regardless of whether your machine is online.
9//
10// HOW IT WORKS:
11// 1. Agent owner schedules a task (e.g., "summarize my inbox every morning at 9am")
12// 2. The task is registered on-chain with a TransactionHandler capability
13// 3. Flow validators execute the handler at the specified timestamp
14// 4. The handler emits an AgentTaskTriggered event
15// 5. The off-chain relay picks up the event and does the actual work
16// 6. Results are posted back on-chain
17//
18// For recurring tasks, the handler RE-SCHEDULES itself after execution,
19// creating a reliable on-chain cron loop that can't be missed.
20//
21// CADENCE FEATURES USED:
22// - Pre/post conditions: validate task parameters, verify execution results
23// - Capabilities: handler references are capability-scoped
24// - Entitlements: separate Schedule, Execute, Cancel permissions
25// - Resources: tasks are owned objects with move semantics
26
27import AgentRegistry from 0x91d0a5b7c9832a8b
28
29access(all) contract AgentScheduler {
30
31    // -----------------------------------------------------------------------
32    // Events
33    // -----------------------------------------------------------------------
34    access(all) event TaskScheduled(
35        taskId: UInt64,
36        agentId: UInt64,
37        owner: Address,
38        taskType: String,
39        executeAt: UFix64,
40        isRecurring: Bool
41    )
42    access(all) event TaskTriggered(
43        taskId: UInt64,
44        agentId: UInt64,
45        owner: Address,
46        taskType: String,
47        triggeredAt: UFix64
48    )
49    access(all) event TaskCompleted(
50        taskId: UInt64,
51        resultHash: String,
52        nextExecutionAt: UFix64?
53    )
54    access(all) event TaskCanceled(taskId: UInt64)
55    access(all) event TaskFailed(taskId: UInt64, reason: String)
56    access(all) event TaskRescheduled(taskId: UInt64, nextExecutionAt: UFix64)
57
58    // -----------------------------------------------------------------------
59    // Paths
60    // -----------------------------------------------------------------------
61    access(all) let SchedulerStoragePath: StoragePath
62
63    // -----------------------------------------------------------------------
64    // State
65    // -----------------------------------------------------------------------
66    access(all) var totalTasks: UInt64
67
68    // -----------------------------------------------------------------------
69    // Entitlements — separate permissions for scheduling vs executing vs canceling
70    // -----------------------------------------------------------------------
71    access(all) entitlement Schedule
72    access(all) entitlement Execute
73    access(all) entitlement Cancel
74    access(all) entitlement Admin
75
76    // -----------------------------------------------------------------------
77    // TaskType — predefined agent task categories
78    // -----------------------------------------------------------------------
79    access(all) enum TaskCategory: UInt8 {
80        // Inference tasks — trigger an LLM call with a prompt
81        access(all) case inference
82        // Memory tasks — compaction, cleanup, summarization
83        access(all) case memoryMaintenance
84        // Monitoring tasks — check conditions, report status
85        access(all) case monitoring
86        // Communication tasks — send scheduled messages
87        access(all) case communication
88        // Custom tasks — user-defined via prompt
89        access(all) case custom
90    }
91
92    // -----------------------------------------------------------------------
93    // RecurrenceRule — how often a task repeats
94    // -----------------------------------------------------------------------
95    access(all) struct RecurrenceRule {
96        access(all) let intervalSeconds: UFix64    // Time between executions
97        access(all) let maxExecutions: UInt64?     // nil = infinite
98        access(all) let endTimestamp: UFix64?      // nil = no end date
99
100        init(
101            intervalSeconds: UFix64,
102            maxExecutions: UInt64?,
103            endTimestamp: UFix64?
104        ) {
105            pre {
106                intervalSeconds >= 60.0: "Minimum interval is 60 seconds"
107            }
108            self.intervalSeconds = intervalSeconds
109            self.maxExecutions = maxExecutions
110            self.endTimestamp = endTimestamp
111        }
112    }
113
114    // -----------------------------------------------------------------------
115    // Common recurrence presets
116    // -----------------------------------------------------------------------
117    access(all) fun everyMinute(): RecurrenceRule {
118        return RecurrenceRule(intervalSeconds: 60.0, maxExecutions: nil, endTimestamp: nil)
119    }
120
121    access(all) fun everyHour(): RecurrenceRule {
122        return RecurrenceRule(intervalSeconds: 3600.0, maxExecutions: nil, endTimestamp: nil)
123    }
124
125    access(all) fun everyDay(): RecurrenceRule {
126        return RecurrenceRule(intervalSeconds: 86400.0, maxExecutions: nil, endTimestamp: nil)
127    }
128
129    access(all) fun everyWeek(): RecurrenceRule {
130        return RecurrenceRule(intervalSeconds: 604800.0, maxExecutions: nil, endTimestamp: nil)
131    }
132
133    // -----------------------------------------------------------------------
134    // TaskConfig — what the scheduled task should do
135    // -----------------------------------------------------------------------
136    access(all) struct TaskConfig {
137        access(all) let name: String               // Human-readable name
138        access(all) let description: String
139        access(all) let category: TaskCategory
140        access(all) let prompt: String             // The instruction for the agent
141        access(all) let targetSessionId: UInt64?   // Which session to use (nil = create new)
142        access(all) let toolsAllowed: [String]     // Which tools this task can use
143        access(all) let maxTurns: UInt64           // Max agentic loop turns
144        access(all) let priority: UInt8            // 0=low, 1=medium, 2=high
145
146        init(
147            name: String,
148            description: String,
149            category: TaskCategory,
150            prompt: String,
151            targetSessionId: UInt64?,
152            toolsAllowed: [String],
153            maxTurns: UInt64,
154            priority: UInt8
155        ) {
156            pre {
157                name.length > 0: "Task name cannot be empty"
158                prompt.length > 0: "Task prompt cannot be empty"
159                maxTurns > 0 && maxTurns <= 20: "Max turns must be 1-20"
160                priority <= 2: "Priority must be 0, 1, or 2"
161            }
162            self.name = name
163            self.description = description
164            self.category = category
165            self.prompt = prompt
166            self.targetSessionId = targetSessionId
167            self.toolsAllowed = toolsAllowed
168            self.maxTurns = maxTurns
169            self.priority = priority
170        }
171    }
172
173    // -----------------------------------------------------------------------
174    // ScheduledTask — a task waiting to be executed
175    // -----------------------------------------------------------------------
176    access(all) struct ScheduledTask {
177        access(all) let taskId: UInt64
178        access(all) let agentId: UInt64
179        access(all) let owner: Address
180        access(all) let config: TaskConfig
181        access(all) let createdAt: UFix64
182        access(all) var nextExecutionAt: UFix64
183        access(all) let recurrence: RecurrenceRule?   // nil = one-shot
184        access(all) var executionCount: UInt64
185        access(all) var lastExecutedAt: UFix64?
186        access(all) var lastResultHash: String?
187        access(all) var status: UInt8                  // 0=active, 1=paused, 2=completed, 3=failed
188
189        init(
190            taskId: UInt64,
191            agentId: UInt64,
192            owner: Address,
193            config: TaskConfig,
194            executeAt: UFix64,
195            recurrence: RecurrenceRule?
196        ) {
197            self.taskId = taskId
198            self.agentId = agentId
199            self.owner = owner
200            self.config = config
201            self.createdAt = getCurrentBlock().timestamp
202            self.nextExecutionAt = executeAt
203            self.recurrence = recurrence
204            self.executionCount = 0
205            self.lastExecutedAt = nil
206            self.lastResultHash = nil
207            self.status = 0
208        }
209    }
210
211    // -----------------------------------------------------------------------
212    // TaskExecutionResult — recorded after each execution
213    // -----------------------------------------------------------------------
214    access(all) struct TaskExecutionResult {
215        access(all) let taskId: UInt64
216        access(all) let executionNumber: UInt64
217        access(all) let triggeredAt: UFix64
218        access(all) let completedAt: UFix64
219        access(all) let resultHash: String
220        access(all) let tokensUsed: UInt64
221        access(all) let turnsUsed: UInt64
222        access(all) let success: Bool
223        access(all) let errorMessage: String?
224
225        init(
226            taskId: UInt64,
227            executionNumber: UInt64,
228            triggeredAt: UFix64,
229            completedAt: UFix64,
230            resultHash: String,
231            tokensUsed: UInt64,
232            turnsUsed: UInt64,
233            success: Bool,
234            errorMessage: String?
235        ) {
236            self.taskId = taskId
237            self.executionNumber = executionNumber
238            self.triggeredAt = triggeredAt
239            self.completedAt = completedAt
240            self.resultHash = resultHash
241            self.tokensUsed = tokensUsed
242            self.turnsUsed = turnsUsed
243            self.success = success
244            self.errorMessage = errorMessage
245        }
246    }
247
248    // -----------------------------------------------------------------------
249    // Scheduler Resource — per-account task manager
250    // -----------------------------------------------------------------------
251    access(all) resource Scheduler {
252        access(all) let agentId: UInt64
253        access(self) var tasks: {UInt64: ScheduledTask}
254        access(self) var executionHistory: [TaskExecutionResult]
255        access(self) var activeTaskCount: UInt64
256
257        init(agentId: UInt64) {
258            self.agentId = agentId
259            self.tasks = {}
260            self.executionHistory = []
261            self.activeTaskCount = 0
262        }
263
264        // --- Schedule: create and manage scheduled tasks ---
265
266        access(Schedule) fun scheduleTask(
267            config: TaskConfig,
268            executeAt: UFix64,
269            recurrence: RecurrenceRule?
270        ): UInt64 {
271            pre {
272                executeAt > getCurrentBlock().timestamp:
273                    "Execution time must be in the future"
274                self.activeTaskCount < 50:
275                    "Maximum 50 active tasks per agent"
276            }
277            post {
278                self.tasks[AgentScheduler.totalTasks] != nil:
279                    "Task must be stored after scheduling"
280            }
281
282            AgentScheduler.totalTasks = AgentScheduler.totalTasks + 1
283            let taskId = AgentScheduler.totalTasks
284
285            let task = ScheduledTask(
286                taskId: taskId,
287                agentId: self.agentId,
288                owner: self.owner!.address,
289                config: config,
290                executeAt: executeAt,
291                recurrence: recurrence
292            )
293
294            self.tasks[taskId] = task
295            self.activeTaskCount = self.activeTaskCount + 1
296
297            emit TaskScheduled(
298                taskId: taskId,
299                agentId: self.agentId,
300                owner: self.owner!.address,
301                taskType: config.name,
302                executeAt: executeAt,
303                isRecurring: recurrence != nil
304            )
305
306            // NOTE: In production, this is where you'd call
307            // FlowTransactionScheduler to register with the protocol.
308            // For the PoC, the relay polls for TaskScheduled events
309            // and sets up its own timer.
310
311            return taskId
312        }
313
314        access(Schedule) fun scheduleRecurringInference(
315            name: String,
316            prompt: String,
317            recurrence: RecurrenceRule,
318            priority: UInt8
319        ): UInt64 {
320            let config = TaskConfig(
321                name: name,
322                description: "Recurring inference: ".concat(name),
323                category: TaskCategory.inference,
324                prompt: prompt,
325                targetSessionId: nil,
326                toolsAllowed: ["memory_store", "memory_recall", "web_fetch"],
327                maxTurns: 5,
328                priority: priority
329            )
330
331            let firstExecution = getCurrentBlock().timestamp + recurrence.intervalSeconds
332
333            return self.scheduleTask(
334                config: config,
335                executeAt: firstExecution,
336                recurrence: recurrence
337            )
338        }
339
340        // --- Execute: trigger and complete tasks ---
341
342        access(Execute) fun triggerTask(taskId: UInt64): Bool {
343            pre {
344                self.tasks[taskId] != nil: "Task not found"
345            }
346
347            if var task = self.tasks[taskId] {
348                // Verify it's time
349                if task.status != 0 {
350                    return false // Not active
351                }
352                if getCurrentBlock().timestamp < task.nextExecutionAt {
353                    return false // Not time yet
354                }
355
356                emit TaskTriggered(
357                    taskId: taskId,
358                    agentId: self.agentId,
359                    owner: self.owner!.address,
360                    taskType: task.config.name,
361                    triggeredAt: getCurrentBlock().timestamp
362                )
363
364                return true
365            }
366            return false
367        }
368
369        access(Execute) fun completeTask(
370            taskId: UInt64,
371            resultHash: String,
372            tokensUsed: UInt64,
373            turnsUsed: UInt64,
374            success: Bool,
375            errorMessage: String?
376        ) {
377            pre {
378                self.tasks[taskId] != nil: "Task not found"
379            }
380            post {
381                // If recurring and successful, must have a next execution time
382                self.tasks[taskId]!.recurrence == nil ||
383                !success ||
384                self.tasks[taskId]!.status != 0 ||
385                self.tasks[taskId]!.nextExecutionAt > before(self.tasks[taskId]!.nextExecutionAt):
386                    "Recurring task must reschedule"
387            }
388
389            if var task = self.tasks[taskId] {
390                let now = getCurrentBlock().timestamp
391
392                // Record execution
393                let result = TaskExecutionResult(
394                    taskId: taskId,
395                    executionNumber: task.executionCount + 1,
396                    triggeredAt: task.nextExecutionAt,
397                    completedAt: now,
398                    resultHash: resultHash,
399                    tokensUsed: tokensUsed,
400                    turnsUsed: turnsUsed,
401                    success: success,
402                    errorMessage: errorMessage
403                )
404                self.executionHistory.append(result)
405
406                // Update task state
407                task = ScheduledTask(
408                    taskId: task.taskId,
409                    agentId: task.agentId,
410                    owner: task.owner,
411                    config: task.config,
412                    executeAt: task.nextExecutionAt,
413                    recurrence: task.recurrence
414                )
415
416                // Handle recurrence
417                if let recurrence = task.recurrence {
418                    let nextExec = now + recurrence.intervalSeconds
419
420                    // Check if we should continue
421                    var shouldContinue = success
422                    if let maxExec = recurrence.maxExecutions {
423                        if task.executionCount + 1 >= maxExec {
424                            shouldContinue = false
425                        }
426                    }
427                    if let endTime = recurrence.endTimestamp {
428                        if nextExec > endTime {
429                            shouldContinue = false
430                        }
431                    }
432
433                    if shouldContinue {
434                        // Reschedule — this is the key to reliable recurring tasks
435                        // The task re-registers itself on-chain for the next execution
436                        emit TaskRescheduled(taskId: taskId, nextExecutionAt: nextExec)
437                    } else {
438                        // Mark as completed
439                        emit TaskCompleted(
440                            taskId: taskId,
441                            resultHash: resultHash,
442                            nextExecutionAt: nil
443                        )
444                    }
445                } else {
446                    // One-shot task — mark complete
447                    self.activeTaskCount = self.activeTaskCount - 1
448                    emit TaskCompleted(
449                        taskId: taskId,
450                        resultHash: resultHash,
451                        nextExecutionAt: nil
452                    )
453                }
454
455                if !success {
456                    emit TaskFailed(taskId: taskId, reason: errorMessage ?? "Unknown error")
457                }
458            }
459        }
460
461        // --- Cancel: stop scheduled tasks ---
462
463        access(Cancel) fun cancelTask(taskId: UInt64): Bool {
464            if self.tasks[taskId] != nil {
465                self.tasks.remove(key: taskId)
466                self.activeTaskCount = self.activeTaskCount - 1
467                emit TaskCanceled(taskId: taskId)
468                return true
469            }
470            return false
471        }
472
473        // --- Read ---
474
475        access(all) fun getTask(taskId: UInt64): ScheduledTask? {
476            return self.tasks[taskId]
477        }
478
479        access(all) fun getActiveTasks(): [ScheduledTask] {
480            var active: [ScheduledTask] = []
481            for taskId in self.tasks.keys {
482                if let task = self.tasks[taskId] {
483                    if task.status == 0 {
484                        active.append(task)
485                    }
486                }
487            }
488            return active
489        }
490
491        access(all) fun getTasksDueBefore(timestamp: UFix64): [ScheduledTask] {
492            var due: [ScheduledTask] = []
493            for taskId in self.tasks.keys {
494                if let task = self.tasks[taskId] {
495                    if task.status == 0 && task.nextExecutionAt <= timestamp {
496                        due.append(task)
497                    }
498                }
499            }
500            return due
501        }
502
503        access(all) fun getExecutionHistory(limit: Int): [TaskExecutionResult] {
504            let len = self.executionHistory.length
505            if len <= limit {
506                return self.executionHistory
507            }
508            return self.executionHistory.slice(from: len - limit, upTo: len)
509        }
510
511        access(all) fun getActiveTaskCount(): UInt64 {
512            return self.activeTaskCount
513        }
514    }
515
516    // -----------------------------------------------------------------------
517    // Public factory
518    // -----------------------------------------------------------------------
519    access(all) fun createScheduler(agentId: UInt64): @Scheduler {
520        return <- create Scheduler(agentId: agentId)
521    }
522
523    // -----------------------------------------------------------------------
524    // Init
525    // -----------------------------------------------------------------------
526    init() {
527        self.totalTasks = 0
528        self.SchedulerStoragePath = /storage/FlowClawScheduler
529    }
530}
531