Smart Contract
AgentScheduler
A.91d0a5b7c9832a8b.AgentScheduler
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