Ethereum smart contracts do not run on a schedule by themselves – there is no native "cron job" within the EVM. Smart contracts are not self-executing, i.e., they require an external stimulus (a transaction from an externally owned account, oracle, or other contract) to call their functions. This presents a challenge for time-dependent actions like token vesting or recurring payments: how do you execute a function at a future time or on a recurring interval without someone manually sending the transaction?
In this tutorial, I'll discuss two of the most popular decentralized automation tools for Ethereum that address this problem: Chainlink Automation (Keepers) and Gelato Network. Both rely on off-chain bot networks or “relayers” to monitor conditions and trigger your contract functions on schedule, removing the need for centralized cron servers. I’ll demonstrate how to automate a simple time-locked token release using Foundry – showing both the Chainlink Keepers approach and the Gelato approach. Along the way, I’ll highlight how each solution works, how to integrate them with your Solidity contracts, and key differences in cost, UX, and developer experience.
The Challenge: Scheduling On-Chain Actions
Unlike traditional servers, blockchains do not allow a program to schedule future function calls themselves. Any state changes must be initiated by a transaction. While developers previously relied on centralized scripts or manual calling of functions at intervals, that is subversive to decentralization and can be unreliable. For example, if you have a token vesting contract that should release tokens to a beneficiary after a time period, some person or entity must call the release()
function at or after the unlock time. It is dangerous to rely on a human or centralized cron job to do this – they can forget or go offline, and it introduces a single point of failure.
Decentralized automation tools solve this by utilizing networks of off-chain agents (bots) that watch for conditions and perform transactions on your behalf. Chainlink Automation (formerly Chainlink Keepers) and Gelato Network are two examples of such services. They run trustworthy, decentralized bot networks that can call into your contract when needed, either on time schedules or on arbitrary logic. Essentially, they are the "cron service" for Ethereum, but in a trust-minimized way:
- Chainlink Automation (Keepers). Uses a network of decentralized Chainlink nodes to monitor your contract conditions (via a
checkUpkeep
function that you supply) and calls aperformUpkeep
function to execute when conditions turn true. Keeper nodes are off-chain and initiate transactions when your pre-defined conditions are met. Your smart contract has the logic of when to execute, and the Chainlink network takes care of calling it at that time if funded. - Gelato Network. Provides a network of relay bots that can be instructed to invoke any contract function at specific times or intervals, or when certain conditions are satisfied. Gelato's bots also run off-chain and submit transactions to execute your task on-chain when the schedule or trigger demands. Gelato offers an easy-to-use "Ops" service where you merely register a task (with a time delay or cron-like schedule, or your own custom condition resolver), and their network will keep calling your function as instructed.
Both solutions remove the need for you to run your own bots or servers. Let's now consider a real-world use case to see how to use each.
Example Use Case: Time-Locked Token Release Contract
To be specific, let's look at an example of a token vesting scenario: an ERC-20 token is vested in a contract and is supposed to be sent to a beneficiary after a particular timestamp. We want this release to happen automatically once the time comes.
First, we’ll write a simple Solidity contract implementing this timelock logic. Then we’ll show how to automate its release()
function with Chainlink Keepers and with Gelato.
Solidity Contract – TokenTimelock.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
// Minimal ERC20 interface for token transfer
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
}
contract TokenTimelock {
IERC20 public immutable token;
address public immutable beneficiary;
uint256 public immutable releaseTime;
bool public released;
constructor(IERC20 tokenAddress, address beneficiaryAddress, uint256 releaseTimestamp) {
token = tokenAddress;
beneficiary = beneficiaryAddress;
releaseTime = releaseTimestamp;
released = false;
}
function release() public {
require(!released, "Already released");
require(block.timestamp >= releaseTime, "Not yet unlocked");
released = true;
// Transfer all tokens held by this contract to the beneficiary
uint256 amount = tokenBalance();
require(token.transfer(beneficiary, amount), "Token transfer failed");
}
function tokenBalance() public view returns (uint256) {
return token.balanceOf(address(this));
}
}
In this contract, we specify the token to release, the beneficiary, and a releaseTime
(as a Unix timestamp). The release()
method will succeed only after the releaseTime
has passed. When called, it marks the state as released and transfers the full token balance to the beneficiary.
We are assuming the contract itself is holding the tokens, maybe because someone deployed it tokens to vest.
Testing the Timelock. With Foundry, we simply write a test that advances time and then calls release()
to check it all works. For example, with Foundry's cheat codes, we can simply test time travel: call vm.warp(block.timestamp + 100)
to advance the blockchain timestamp by 100 seconds in a test. This allows you to assert release()
fails before the timestamp and succeeds later. In reality, however, who will call release()
at the right moment? This is where automation steps in.
Automating with Chainlink Automation (Keepers)
Chainlink Keepers (now Chainlink Automation) provide decentralized off-chain bots which will monitor your contract and invoke a function at the right time. To use Chainlink Automation, you need to make your contract Keeper-compatible by implementing two functions: checkUpkeep
and performUpkeep
. The Chainlink nodes will call checkUpkeep
off-chain on every block (or at a specified interval) to see if your conditions are met, and if it returns true
, they trigger an on-chain transaction to performUpkeep
.
Let’s modify our TokenTimelock
contract to integrate with Chainlink Automation:
import "@chainlink/contracts/src/v0.8/interfaces/KeeperCompatibleInterface.sol";
contract TokenTimelock is KeeperCompatibleInterface {
// ... (previous state variables and constructor)
// Implement Chainlink Keepers interface:
function checkUpkeep(bytes calldata /* checkData */) external view override returns (bool upkeepNeeded, bytes memory /* performData */) {
upkeepNeeded = (!released && block.timestamp >= releaseTime);
}
function performUpkeep(bytes calldata /* performData */) external override {
// Revalidate the condition on-chain to be safe
if (!released && block.timestamp >= releaseTime) {
release(); // call the release function
}
}
}
We import Chainlink’s KeeperCompatibleInterface
(part of Chainlink’s contract library) and implement it. In checkUpkeep
, we simply return true if the tokens haven’t been released yet and the current time is past the releaseTime
. In performUpkeep
, we include a safety check to ensure the condition still holds, then call the release()
function. Chainlink’s nodes will simulate checkUpkeep
off-chain and, when it returns true, one of the nodes will send a transaction to call performUpkeep
on-chain.
Re-checking the condition in performUpkeep
is a recommended best practice to handle edge cases or race conditions.
Deploying the contract. You can deploy this contract to a test network using Foundry (for example, using forge script
or forge create
). Make sure to fund the contract with the tokens that need to be released (so it has a balance to send). Once deployed, note the contract address.
Registering an upkeep. Next, you need to register your contract with Chainlink Automation so the Keepers network knows about it. Go to the Chainlink Automation app (the Chainlink Keepers dashboard) and register a new Upkeep:
- Define target contract. Select a trigger type. In our case, since the contract itself checks for the time condition, we can use a time-based trigger (e.g. schedule to run periodically) or simply a "Custom logic" trigger. Chainlink now even has a Cron-style scheduling option for upkeeps. You can, for example, select Cron and enter an expression like
*/5 * * * *
so it attempts every 5 minutes. Or you can mention a time-based Cron close to the release date. Provide the deployedTokenTimelock
contract address and ABI. This tells the network to call your contract. - Function and Input. If using the Automation Job Scheduler for time triggers, you directly specify the
release()
function as the target to call on schedule. However, since our contract uses the Keeper interface, the standard flow is that the Chainlink network will callcheckUpkeep
/performUpkeep
. When you are in Keepers UI, after you add the ABI, generally you select the upkeep functions. For a time trigger on the new UI, you can simply callperformUpkeep
at a regular interval. In the background, the Chainlink nodes will keep on callingcheckUpkeep
to confirmupkeepNeeded
is true prior to acting. As a simplification, consider the Function to be automated as ourcheckUpkeep
condition on a schedule. - Schedule. If you're utilizing a Cron trigger, enter the schedule expression when to run checks. For a one-time future run, you might not have a built-in one-time option – one simple way is to schedule frequent checks (e.g., every X minutes) for close to that time so one of them will catch the condition as true. Chainlink's Automation Job Scheduler UI came in 2022 to make time-based scheduling simpler. For our case, as
checkUpkeep
will be false until the time of unlocking, you can schedule it to run, for example, every 15 minutes. At the time condition being met, the next check will become true and trigger the upkeep. - Upkeep details. Give your upkeep a name and specify a gas limit (how much gas you expect the
performUpkeep
to use). Specify the initial balance — you will need to fund with LINK tokens to pay for the automated service fee. Chainlink Automation requires upkeep to be funded in LINK. The nodes use this to pay for the gas and take a fee. Testnets can have test LINK provided from a faucet. On mainnet, you’d obtain and deposit real LINK. Finally, register the upkeep.
Once registered, Chainlink’s decentralized keeper network takes over. The nodes will automatically start checking your contract. When the releaseTime
passes, checkUpkeep
will start returning true, and a transaction will be sent to call performUpkeep
which in turn executes release()
. Your beneficiary should receive the tokens shortly after the scheduled time without any manual intervention.
Testing with Foundry
Off-chain behavior of Chainlink is hard to fully replicate locally, but you can simulate it in tests. For example, call checkUpkeep
in a test within Foundry so that it initially returns false, use vm.warp
to advance time beyond releaseTime
, then call checkUpkeep
again to see it return true. You can also call performUpkeep
(or release()
) directly within a test after warping time to ensure the effect. In practice, on a real network, the Chainlink nodes call these for you at the appropriate time.
Note. There is something you should keep in mind, which is security – our release()
function is public, so anyone can call it when the time is appropriate. This is generally okay (the net effect is the same token transfer to beneficiary), and it allows Chainlink Keepers (even an innocent user) to initiate the payout. If you really just wanted the Chainlink Registry to invoke it, add access control to performUpkeep
(the address of the Keeper registry is known per network). But overall, declaring the function public with proper time checks is fine because it cannot be called twice or earlier. Also, ensure the contract has enough gas limit in the maintenance registration to cover the token transfer.
Automating with Gelato Network (Gelato Ops)
Gelato Network offers an equivalent product but different model and developer experience. Gelato's Automate (Ops) function allows you to automate calls to contract functions through their web interface or code. You don't need to rearchitect your contract to automate based on time-based triggers – any external function is addressable. This makes Gelato highly convenient for automating on live contracts. Gelato's network of bots will carry out the task in accordance with your set schedule or condition as a decentralized relayer.
For our TokenTimelock
example, as it already has a publicly accessible release()
function time-guarded, we can use Gelato to call release()
immediately after releaseTime
elapses.
Using Gelato Ops dashboard:
-
Set Up Gelato. Go to the Gelato Ops dashboard (app.gelato.network) and sign in using your Web3 wallet. Choose the network (Ethereum mainnet or a testnet) where you have your contract deployed. Gelato works with numerous EVM-compatible networks, like Chainlink.
-
Funding for Execution. Gelato tasks require paying for gas (and a small fee) to incentivize the executors. Gelato supports two payment modes: you can pre-fund your Gelato balance with ETH (or another token) that will be used by Gelato's bots to carry out your transactions, or you can use a "pay per task" model (Gelato calls this 1Balance/SyncFee model) where the bot charges a tiny fee that your contract or sponsor pays when the task gets executed. Go to the dashboard and go to the Funds section and prepay some ETH if you plan on prepaying. For one-off operations, you can just pay per execution instead of contributing a lot initially.
-
Create a task. Press Create Task (or analogous link in Gelato Ops). You will be asked for:
-
Target contract address. Your
TokenTimelock
contract address. -
ABI. Insert the ABI JSON of the contract. This allows the UI to show available functions.
-
Function to automate. Select the
release()
function from the ABI function list. -
Schedule/Trigger. Choose a trigger to run with. Gelato offers a few different types of triggers. For simple time-based running, you have a couple of choices:
- Time Interval: e.g. "every 1 hour" or "every day at 00:00". This is like a repeat schedule. If a one-off release is our goal, a repeat interval won't work unless we stop the task ourselves after it's run.
- Cron (Scheduled advanced): Gelato also provides the facility of cron expressions or a specific time. You can schedule a cron for the exact time when you want to release. If you have knowledge about the time of unlock, create a cron for that minute/hour. Ensure the cron is written in UTC time and in the proper format.
- Conditional (Resolver). Gelato also allows a custom logic trigger using a resolver contract. It is similar to Chainlink's check/perform separation. You can deploy a plain resolver contract with a method like
bool ready = (block.timestamp >= releaseTime && !released)
and then use that as a condition. But in our case, sincerelease()
will revert if not ready, we can simply rely on timing.
-
For convenience, if the run is to be performed once, you can select a Cron trigger for the unlock time, or you can set a fixed interval (say, every 5 minutes) and have a toggle to start at a specific time. The Gelato UI has a "Start immediately" checkbox – if you clear that check mark, you can set a start time for the job. Fix that start time to your
releaseTime
(or a bit later, to be safe). If using a time interval like 5 minutes, also set the task to complete after one run (if the UI will let you) or intend to cancel it manually. In most situations, developers simply create the task a short time before the target time and then cancel it manually after one run.
-
-
Task details. Select the payment method for the task. If you funded your balance in step 2, use Balance as payment. Alternatively, select automatic (deduct fee) if you'd like Gelato to deduct the fee from the task itself (this may necessitate your contract to be holding ETH or the token specified to pay the fee). Give the task a name for your records.
-
Create and confirm. Press "Create Task" and confirm the transaction that appears (if any). On initialization, Gelato ask you to authorize the addition of Gelato's automate contract as a spender of funds (especially when using ERC-20 fees) or other permission. If everything is set up, you should now see the new task listed in the dashboard along with its status.
Upon turning on the Gelato task, Gelato bots will take care of monitoring. At the designated time (or period), Gelato will call release()
on your contract. Because our contract's release()
will be effective only after releaseTime
, the very first try at or after the unlock time will succeed and relocate the tokens. If we set it as a one-time cron or if we manually kill the job afterward, no further calls will be executed. If we made it a recurring task, subsequent calls would simply see released = true
and likely do nothing (perhaps revert or simply waste gas), so it's better to cancel or throttle instead. Gelato's dashboard allows cancelling tasks, as (unlike Chainlink upkeeps) Gelato tasks can't be modified once created (you'd have to cancel and create one anew for changes).
Allow contract calls (if needed). Our release()
function was public and didn't restrict who could call it (aside from the timing check). This is useful because we don't need to whitelist Gelato – anyone calling it after the time will simply perform the transfer to the beneficiary. If, instead, our function had an owner or specific caller that it required, we would need to authorize Gelato to call it. Gelato can handle such scenarios either through the use of a dedicated Gelato executor address or by initiating a proxy that you approve. But for most timelock scenarios, making the function publicly callable is acceptable and even desirable (any type of keeper can initiate it as soon as requirements are met).
Monitoring. The Gelato dashboard will show execution logs. After the elapsed time, you can see a transaction from Gelato's executor address to your contract. You can verify on Etherscan that release()
was called and tokens were transferred. Gelato also offers notification integrations (e.g., on Discord/Telegram) to alert you if a task failed or when your balance is low.
Chainlink Keepers vs. Gelato Automate Comparison
Chainlink Automation and Gelato share the goal of trustlessly automating contract calls, but with different strategy and trade-offs. The comparison below is brief on some points:
- Integration effort. Chainlink requires you to implement your smart contract through the
checkUpkeep
/performUpkeep
interface (or use their new no-code scheduler for simple time-based calls). That means writing additional code and deploying a custom contract that is Keepers-compatible. Gelato, however, can automate any existing contract function without needing code modifications – you simply define a task referencing a function. This makes Gelato's architecture extremely intuitive to use for retrofitting automation. Gelato, however, also includes an advanced feature for using resolver contracts if you want complex condition logic divorced from the execution logic. - Payment and cost. Chainlink Automation work is rewarded in LINK tokens. You need to fund LINK (ERC-677 on Ethereum) in the Keeper registry to fund your upkeep. This introduces LINK token reliance and management (and on non-Ethereum chains, LINK has to be bridged, since it's not natively on those networks). Gelato tasks are paid in USDC. You can choose prepaying or pay-per-execution. Gelato doesn't even charge you an extra protocol fee on top of gas – you pretty much just pay the gas for the transaction and maybe a small premium to the executor, and you can even have that subtracted from the task itself. With Chainlink, you're paying gas and a small premium on the network from your LINK deposit. Both are fairly inexpensive in practice for large tasks, though Gelato is simpler if you prefer to pay gas with USDC itself.
- Reliability and decentralization. Chainlink's Keepers network is highly decentralized and built on the same reliable node operators powering Chainlink oracles. It has a good track record (Chainlink Keepers have been used by 100+ projects since mainnet launch). Gelato is also decentralized, with a network of executors (though as of early days it had a whitelisted set of node operators, it’s moving toward permissionless decentralization. Both are looking for high reliability with fallbacks to ensure transactions go through. Chainlink rotates which node does an upkeep to avoid competition and keep cost low. Gelato does not charge additional fees currently and even uses Flashbots to avoid your transactions getting frontrunned, which is a good bonus.
- Developer experience (UX & tooling). Chainlink has decent documentation and fairly straightforward registration UI. However, setting up a Keeper does require deploying new contract code if your contract wasn’t initially designed for it. Testing can be done by simulating
checkUpkeep
in a dev environment. Gelato's UX is very welcoming – the Gelato Ops dashboard is a no-code panel where you point-and-click to schedule contract calls. This is ideal for quick automation or for non-technical developers. Gelato also offers an SDK and even assistance to program tasks directly in Solidity (Gelato also has a contract API to schedule tasks from within other contracts). Chainlink has also recognized the need for ease-of-use by introducing the Automation Job Scheduler UI, which similarly lets you schedule time-based calls without writing the check logic (essentially converging on a more direct scheduling approach for simple cases). In terms of community and support, both are active: Chainlink is very widely adopted and supported, and Gelato’s documentation is also solid. - Flexibility of triggers. Chainlink’s model shines for condition-based triggers that involve on-chain data – you encode the condition in
checkUpkeep
. E.g., "if my contract balance falls below X" or "if oracle price exceeds Y". Gelato can do similar logic using resolver contracts, but Chainlink's off-chain simulation is gas-friendly (since you only get chargedcheckUpkeep
calls once they are executed). For vanilla time-based scheduling, both will do the job just as well. Gelato's cron and interval triggers and Chainlink's cron scheduling are identical. Advantage of Gelato is that you may have condition and action as two different contracts if needed (resolver and executor), which can avoid modifying original contracts.
All in all, Chainlink Automation is a great choice if you require a very decentralized solution and don't mind introducing their interface to your contract (or already have it deployed). It's thoroughly tested and widely used in production. Gelato possesses a very easy-to-use and versatile platform for automation that is perfect for scheduling calls on already existing contracts or when you would like to automate between multiple chains with one tool. Many developers even use both, depending on context – for instance, Chainlink for critical on-chain condition upkeeps, and Gelato for simpler scheduled tasks or off-chain integrated workflows.
Conclusion
As a developer using Foundry, you can integrate these services and even test the time-based logic by simulating time in your Forge tests. When deploying to live networks, the heavy lifting of monitoring and execution is handled by decentralized infrastructure rather than your own scripts. Automation tools like Chainlink and Gelato improve security and UX by removing the reliance on manual triggers. They enable use cases from periodic reward payouts, to rebasing tokens, to executing trades when conditions meet, all without human intervention.