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:

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.

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:

  1. 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 deployed TokenTimelock contract address and ABI. This tells the network to call your contract.
  2. 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 call checkUpkeep/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 call performUpkeep at a regular interval. In the background, the Chainlink nodes will keep on calling checkUpkeep to confirm upkeepNeeded is true prior to acting. As a simplification, consider the Function to be automated as our checkUpkeep condition on a schedule.
  3. 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.
  4. 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:

  1. 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.

  2. 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.

  3. Create a task. Press Create Task (or analogous link in Gelato Ops). You will be asked for:

    1. Target contract address. Your TokenTimelock contract address.

    2. ABI. Insert the ABI JSON of the contract. This allows the UI to show available functions.

    3. Function to automate. Select the release() function from the ABI function list.

    4. 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:

      1. 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.
      2. 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.
      3. 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, since release() will revert if not ready, we can simply rely on timing.
    5. 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.

  4. 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.

  5. 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 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:

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.