Cron-Based Recurring Transactions
Sometimes you need blockchain logic to run automatically on a schedule: distributing rewards every day, checking conditions every hour, or processing batches every week. Instead of manually triggering these transactions, you can automate them.
Cron is a time-based scheduling system originally from Unix. It lets you define "run this at 9am every Monday" using a simple pattern called a cron expression. The FlowCron smart contract brings this same concept onchain, so you can schedule recurring transactions that run automatically without any external triggers.
For example, with the cron expression 0 0 * * * (daily at midnight), your transaction executes every day at midnight UTC—indefinitely—until you stop it.
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 your existing TransactionHandler. You give it a cron expression (like */5 * * * * for every 5 minutes) and your handler, and FlowCron takes care of the rest. Once started, your schedule runs indefinitely without any further action from you.
Why Two Transactions?
A key challenge with recurring schedules is fault tolerance: what happens if your code has a bug? You don't want one failed execution to break the entire schedule.
FlowCron solves this by running two separate transactions each time your cron triggers:
- Executor: Runs your code. If your logic fails, only this transaction reverts.
- Keeper: Schedules the next cycle. Runs independently, so even if your code throws an error, the schedule continues.
The benefit: Your recurring schedule won't break if your TransactionHandler execution fails. The keeper always ensures the next execution is scheduled, regardless of whether the current one succeeded or failed.
_10Timeline ─────────────────────────────────────────────────────────>_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
A cron expression is just five numbers (or wildcards) that define when something should run.
FlowCron uses the standard 5-field cron format:
_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)
| Pattern | When it runs |
|---|---|
* * * * * | Every minute |
*/5 * * * * | Every 5 minutes |
0 * * * * | Top of every hour |
0 0 * * * | Daily at midnight |
0 0 * * 0 | Weekly on Sunday |
0 9-17 * * 1-5 | Hourly, 9am-5pm weekdays |
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 four steps:
- Create a handler: Write the code you want to run on each tick
- Wrap it with FlowCron: Connect your handler to a cron schedule
- Start the schedule: Kick off the first execution
- Monitor: Check that everything is running
All transactions and scripts referenced below are available in the FlowCron GitHub repository.
Prerequisites
Before you start, make sure you have:
- Flow CLI installed: This is the command-line tool you'll use to deploy contracts, send transactions, and run scripts. If you don't have it yet, follow the installation guide.
- FLOW tokens for transaction fees: Every transaction costs a small amount of FLOW. Get free testnet FLOW from the Faucet.
- A Flow account: The CLI will help you create one if you don't have one yet.
1. Create Your Handler
First, you need to write the code that will run on each scheduled tick. In Cadence, this is called a TransactionHandler. A TransactionHandler is a resource that implements the FlowTransactionScheduler.TransactionHandler interface.
The key part is the executeTransaction function. This is where you put whatever logic you want to run on schedule: updating state, distributing tokens, checking conditions, etc.
For more details on how handlers work, see the Scheduled Transactions documentation.
Here's a simple example contract:
_32import "FlowTransactionScheduler"_32_32access(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}
This example handler simply logs the timestamp when executed. Replace the log statement with your own logic.
Deploy Your Contract
Use the Flow CLI to deploy your TransactionHandler contract:
_10flow project deploy --network=testnet
This command reads your flow.json configuration and deploys all configured contracts. If you're new to deploying, see the deployment guide for a complete walkthrough.
Create and Store a Handler Instance
After deploying, you need to create an instance of your handler and save it to your account's storage. The storage is an area in your account where you can save resources (like your handler) that persist between transactions.
See CounterTransactionHandler.cdc for a complete working example that includes the storage setup.
2. Wrap It with FlowCron
Now you need to wrap your handler with a CronHandler. This connects your handler to a cron schedule.
The following transaction creates a new CronHandler resource that holds your cron expression and a reference to your handler:
_21transaction(_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.
Send this transaction using the Flow CLI:
_10flow transactions send CreateCronHandler.cdc \_10 "*/5 * * * *" \_10 /storage/MyRecurringTaskHandler \_10 /storage/MyCronHandler \_10 --network=testnet
The arguments are: your cron expression, the storage path where your handler lives, and the path where the new CronHandler will be stored.
3. Start the Schedule
This transaction schedules the first executor and keeper, which kicks off the self-perpetuating loop. After this, your cron job runs automatically:
_31transaction(_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.
Send this transaction using the Flow CLI:
_10flow transactions send ScheduleCronHandler.cdc \_10 /storage/MyCronHandler \_10 nil \_10 2 \_10 500 \_10 2500 \_10 --network=testnet
Parameters:
| Parameter | Description |
|---|---|
cronHandlerStoragePath | Path to your CronHandler |
wrappedData | Optional data passed to handler (nil or your data) |
executorPriority | 0 (High), 1 (Medium), or 2 (Low) |
executorExecutionEffort | Computation units for your code (start with 500) |
keeperExecutionEffort | Computation units for keeper (use 2500) |
Starting a cron job requires prepaying fees for the scheduled transactions. FLOW will be deducted from your account to cover the executor and keeper fees. Make sure you have enough FLOW before running this transaction.
Once this transaction succeeds, your cron job is live. The first execution will happen at the next cron tick, and the schedule will continue automatically from there. You don't need to do anything else, unless you want to monitor it or stop it.
4. Check Status
Use Cadence scripts to check your cron job's status. Scripts are read-only queries that inspect blockchain state without submitting a transaction—they're free to run and don't modify anything. Learn more in the scripts documentation.
Query Cron Info
The GetCronInfo.cdc script returns metadata about your cron handler:
_10access(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}
Run this script using the Flow CLI:
_10flow scripts execute GetCronInfo.cdc 0xYourAddress /storage/MyCronHandler --network=testnet
If your cron job is running, you'll see output showing the cron expression, next scheduled execution time, and handler status.
Calculate Next Execution Time
The GetNextExecutionTime.cdc script calculates when your cron expression will next trigger:
_10access(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}
Run this script using the Flow CLI:
_10flow scripts execute GetNextExecutionTime.cdc "*/5 * * * *" nil --network=testnet
Additional Debugging Scripts
- GetCronScheduleStatus.cdc — Returns executor/keeper transaction IDs, timestamps, and current status
- GetParsedCronExpression.cdc — Validates and parses a cron expression into a
CronSpec
Stopping a Cron Job
You might want to stop a cron job for several reasons:
- Debugging: Something isn't working and you need to investigate
- Updating: You want to change the schedule or handler logic
- Cost: You no longer need the recurring execution
- Temporary pause: You want to stop temporarily and restart later
To stop a running cron job, you need to cancel both the pending executor and keeper transactions. This transaction retrieves the scheduled transaction IDs from your CronHandler and cancels them:
_21transaction(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.
Send this transaction using the Flow CLI:
_10flow transactions send CancelCronSchedule.cdc /storage/MyCronHandler --network=testnet
Cancelling refunds 50% of the prepaid fees back to your account.
Contract Addresses
FlowCron is deployed on both Testnet and Mainnet:
| Contract | Testnet | Mainnet |
|---|---|---|
| FlowCron | 0x5cbfdec870ee216d | 0x6dec6e64a13b881e |
| FlowCronUtils | 0x5cbfdec870ee216d | 0x6dec6e64a13b881e |