LLM Notice: This documentation site supports content negotiation for AI agents. Request any page with Accept: text/markdown or Accept: text/plain header to receive Markdown instead of HTML. Alternatively, append ?format=md to any URL. All markdown files are available at /md/ prefix paths. For all content in one file, visit /llms-full.txt
Skip to main content

Cron-based Recurring Transactions

On traditional systems, cron jobs handle recurring executions. On Flow, the FlowCron smart contract brings the same familiar cron syntax onchain. You define a schedule like 0 0 * * * (daily at midnight) using a cron expression, and your transaction runs countinously based on the schedule.

info

FlowCron builds on Flow's Scheduled Transactions. If you haven't worked with scheduled transactions before, check out the Scheduled Transactions documentation first.

How It Works

FlowCron provides a CronHandler resource that wraps any existing TransactionHandler. You give it a cron expression and your handler, and it takes care of scheduling and perpetuating the execution cycle. Once started, the schedule runs indefinitely without further intervention.

Under the hood, the CronHandler runs two types of transactions per tick to ensure fault tolerance:

  • Executor: Runs your code. If your logic fails, only this transaction reverts.
  • Keeper: Schedules the next cycle. Runs independently so the schedule survives even if your code throws an error.

This separation keeps the recurring loop alive regardless of what happens in your handler.


_10
Timeline ─────────────────────────────────────────────────────────>
_10
T1 T2 T3
_10
│ │ │
_10
├── Executor ──────────►├── Executor ──────────►├── Executor
_10
│ (runs user code) │ (runs user code) │ (runs user code)
_10
│ │ │
_10
└── Keeper ────────────►└── Keeper ────────────►└── Keeper
_10
(schedules T2) (schedules T3) (schedules T4)
_10
(+1s offset) (+1s offset) (+1s offset)

Cron Expressions

FlowCron uses the standard 5-field cron format that you may already know from Unix systems:


_10
┌───────────── minute (0-59)
_10
│ ┌───────────── hour (0-23)
_10
│ │ ┌───────────── day of month (1-31)
_10
│ │ │ ┌───────────── month (1-12)
_10
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
_10
│ │ │ │ │
_10
* * * * *

Operators: * (any), , (list), - (range), / (step)

PatternWhen it runs
* * * * *Every minute
*/5 * * * *Every 5 minutes
0 * * * *Top of every hour
0 0 * * *Daily at midnight
0 0 * * 0Weekly on Sunday
0 9-17 * * 1-5Hourly, 9am-5pm weekdays
note

When you specify both day-of-month and day-of-week (not *), the job runs if either matches. So 0 0 15 * 0 fires on the 15th OR on Sundays.

Setup

Setting up a cron job involves creating a handler for your logic, wrapping it with FlowCron and scheduling the first tick. All transactions and scripts referenced below are available in the FlowCron GitHub repository.

Before you start, make sure you have:

  • Flow CLI installed
  • Some FLOW for transaction fees
  • A TransactionHandler containing your recurring logic

1. Create Your Handler

Create a contract that implements the TransactionHandler interface:


_32
import "FlowTransactionScheduler"
_32
_32
access(all) contract MyRecurringTask {
_32
_32
access(all) resource Handler: FlowTransactionScheduler.TransactionHandler {
_32
_32
access(FlowTransactionScheduler.Execute)
_32
fun executeTransaction(id: UInt64, data: AnyStruct?) {
_32
// Your logic here
_32
log("Cron fired at ".concat(getCurrentBlock().timestamp.toString()))
_32
}
_32
_32
access(all) view fun getViews(): [Type] {
_32
return [Type<StoragePath>(), Type<PublicPath>()]
_32
}
_32
_32
access(all) fun resolveView(_ view: Type): AnyStruct? {
_32
switch view {
_32
case Type<StoragePath>():
_32
return /storage/MyRecurringTaskHandler
_32
case Type<PublicPath>():
_32
return /public/MyRecurringTaskHandler
_32
default:
_32
return nil
_32
}
_32
}
_32
}
_32
_32
access(all) fun createHandler(): @Handler {
_32
return <- create Handler()
_32
}
_32
}

Deploy this and save a handler instance to storage. See CounterTransactionHandler.cdc for a working example.

2. Wrap It with FlowCron

Create a CronHandler that wraps your handler with a cron expression:


_21
transaction(
_21
cronExpression: String,
_21
wrappedHandlerStoragePath: StoragePath,
_21
cronHandlerStoragePath: StoragePath
_21
) {
_21
prepare(acct: auth(BorrowValue, IssueStorageCapabilityController, SaveValue) &Account) {
_21
// Issue capability for wrapped handler
_21
let wrappedHandlerCap = acct.capabilities.storage.issue<
_21
auth(FlowTransactionScheduler.Execute) &{FlowTransactionScheduler.TransactionHandler}
_21
>(wrappedHandlerStoragePath)
_21
_21
// Create and save the CronHandler
_21
let cronHandler <- FlowCron.createCronHandler(
_21
cronExpression: cronExpression,
_21
wrappedHandlerCap: wrappedHandlerCap,
_21
feeProviderCap: feeProviderCap,
_21
schedulerManagerCap: schedulerManagerCap
_21
)
_21
acct.storage.save(<-cronHandler, to: cronHandlerStoragePath)
_21
}
_21
}

See CreateCronHandler.cdc for the full transaction. Run it with:


_10
flow transactions send CreateCronHandler.cdc \
_10
"*/5 * * * *" \
_10
/storage/MyRecurringTaskHandler \
_10
/storage/MyCronHandler

3. Start the Schedule

Schedule the first executor and keeper to kick off the perpetual loop:


_31
transaction(
_31
cronHandlerStoragePath: StoragePath,
_31
wrappedData: AnyStruct?,
_31
executorPriority: UInt8,
_31
executorExecutionEffort: UInt64,
_31
keeperExecutionEffort: UInt64
_31
) {
_31
prepare(signer: auth(BorrowValue, IssueStorageCapabilityController, SaveValue) &Account) {
_31
// Calculate next cron tick time
_31
let cronHandler = signer.storage.borrow<&FlowCron.CronHandler>(from: cronHandlerStoragePath)
_31
?? panic("CronHandler not found")
_31
let executorTime = FlowCronUtils.nextTick(spec: cronHandler.getCronSpec(), afterUnix: currentTime)
_31
}
_31
_31
execute {
_31
// Schedule executor (runs your code)
_31
self.manager.schedule(
_31
handlerCap: self.cronHandlerCap,
_31
data: self.executorContext,
_31
timestamp: UFix64(self.executorTime),
_31
...
_31
)
_31
// Schedule keeper (schedules next cycle)
_31
self.manager.schedule(
_31
handlerCap: self.cronHandlerCap,
_31
data: self.keeperContext,
_31
timestamp: UFix64(self.keeperTime),
_31
...
_31
)
_31
}
_31
}

See ScheduleCronHandler.cdc for the full transaction. Run it with:


_10
flow transactions send ScheduleCronHandler.cdc \
_10
/storage/MyCronHandler \
_10
nil \
_10
2 \
_10
500 \
_10
2500

Parameters:

ParameterDescription
cronHandlerStoragePathPath to your CronHandler
wrappedDataOptional data passed to handler (nil or your data)
executorPriority0 (High), 1 (Medium), or 2 (Low)
executorExecutionEffortComputation units for your code (start with 500)
keeperExecutionEffortComputation units for keeper (use 2500)

4. Check Status

Query your cron job's metadata with GetCronInfo.cdc:


_10
access(all) fun main(handlerAddress: Address, handlerStoragePath: StoragePath): FlowCron.CronInfo? {
_10
let account = getAuthAccount<auth(BorrowValue) &Account>(handlerAddress)
_10
if let handler = account.storage.borrow<&FlowCron.CronHandler>(from: handlerStoragePath) {
_10
return handler.resolveView(Type<FlowCron.CronInfo>()) as? FlowCron.CronInfo
_10
}
_10
return nil
_10
}


_10
flow scripts execute GetCronInfo.cdc 0xYourAddress /storage/MyCronHandler

Calculate when the next tick will occur with GetNextExecutionTime.cdc:


_10
access(all) fun main(cronExpression: String, afterUnix: UInt64?): UFix64? {
_10
let cronSpec = FlowCronUtils.parse(expression: cronExpression)
_10
if cronSpec == nil { return nil }
_10
let nextTime = FlowCronUtils.nextTick(
_10
spec: cronSpec!,
_10
afterUnix: afterUnix ?? UInt64(getCurrentBlock().timestamp)
_10
)
_10
return nextTime != nil ? UFix64(nextTime!) : nil
_10
}


_10
flow scripts execute GetNextExecutionTime.cdc "*/5 * * * *" nil

Additional scripts for debugging:

Stopping a Cron Job

To stop a running cron job, cancel both the executor and keeper transactions:


_21
transaction(cronHandlerStoragePath: StoragePath) {
_21
prepare(signer: auth(BorrowValue, IssueStorageCapabilityController, SaveValue) &Account) {
_21
let cronHandler = signer.storage.borrow<&FlowCron.CronHandler>(from: cronHandlerStoragePath)
_21
?? panic("CronHandler not found")
_21
_21
self.executorID = cronHandler.getNextScheduledExecutorID()
_21
self.keeperID = cronHandler.getNextScheduledKeeperID()
_21
}
_21
_21
execute {
_21
// Cancel executor and keeper, receive fee refunds
_21
if let id = self.executorID {
_21
let refund <- self.manager.cancel(id: id)
_21
self.feeReceiver.deposit(from: <-refund)
_21
}
_21
if let id = self.keeperID {
_21
let refund <- self.manager.cancel(id: id)
_21
self.feeReceiver.deposit(from: <-refund)
_21
}
_21
}
_21
}

See CancelCronSchedule.cdc for the full transaction. Run it with:


_10
flow transactions send CancelCronSchedule.cdc /storage/MyCronHandler

Cancelling refunds 50% of the prepaid fees back to your account.

Contract Addresses

FlowCron is deployed on both Testnet and Mainnet:

ContractTestnetMainnet
FlowCron0x5cbfdec870ee216d0x6dec6e64a13b881e
FlowCronUtils0x5cbfdec870ee216d0x6dec6e64a13b881e

Resources