DEV Community

Jakhongir Tashpulatov for Thyme Labs

Posted on

Gelato Web3 Functions shut down. Here's how to port yours

Gelato ended Web3 Functions and Automate on March 31, 2026. Every task created there stopped: the triggers, the recurring jobs, the logs. If a contract of yours depended on one, it has been waiting for an executor since spring.

We run Thyme Flow, which does the same kind of job: a TypeScript function that reads the chain, decides, and returns the calls to send. This post shows a real port, the few places where things differ, and how to hand the whole job to a coding agent.

The short version

Your decision logic does not change. Three things do:

  1. The entry point: Web3Function.onRun(...) becomes defineTask({ schema, run }).
  2. The provider: ethers via multiChainProvider becomes ctx.client, a read-only viem client.
  3. One field name: callData becomes calls.

{ canExec: false, message } stays exactly as it was.

Before: a Gelato oracle function

import { Web3Function, Web3FunctionContext } from '@gelatonetwork/web3-functions-sdk'
import { Contract } from '@ethersproject/contracts'

const ORACLE_ABI = [
  'function lastUpdated() view returns (uint256)',
  'function updatePrice(uint256)',
]

Web3Function.onRun(async (context: Web3FunctionContext) => {
  const { userArgs, multiChainProvider, secrets } = context
  const provider = multiChainProvider.default()
  const oracle = new Contract(userArgs.oracle as string, ORACLE_ABI, provider)

  const lastUpdated = Number(await oracle.lastUpdated())
  if (Date.now() / 1000 < lastUpdated + 3600) {
    return { canExec: false, message: 'Time not elapsed' }
  }

  const price = await fetchPrice(await secrets.get('PRICE_API_KEY'))

  return {
    canExec: true,
    callData: [{
      to: userArgs.oracle as string,
      data: oracle.interface.encodeFunctionData('updatePrice', [price]),
    }],
  }
})
Enter fullscreen mode Exit fullscreen mode

After: the same function on Thyme Flow

import { defineTask, z } from '@thyme-labs/sdk'
import { encodeFunctionData, parseAbi } from 'viem'

const abi = parseAbi([
  'function lastUpdated() view returns (uint256)',
  'function updatePrice(uint256)',
])

export default defineTask({
  schema: z.object({ oracle: z.address() }),
  async run(ctx) {
    const lastUpdated = await ctx.client.readContract({
      address: ctx.args.oracle,
      abi,
      functionName: 'lastUpdated',
    })
    if (Date.now() / 1000 < Number(lastUpdated) + 3600) {
      return { canExec: false, message: 'Time not elapsed' }
    }

    const price = await fetchPrice(ctx.secrets.PRICE_API_KEY)

    return {
      canExec: true,
      calls: [{
        to: ctx.args.oracle,
        data: encodeFunctionData({
          abi,
          functionName: 'updatePrice',
          args: [price],
        }),
      }],
    }
  },
})
Enter fullscreen mode Exit fullscreen mode

Same check, same API call, same call returned. The arguments are now validated by a Zod schema before run starts, and the console renders a form from it.

Mapping the rest

Gelato Thyme Flow
userArgs + schema.json ctx.args, typed by Zod
await storage.get(key) / storage.set(key, value) ctx.storage.key = value (plain JSON, not only strings)
await secrets.get('KEY') ctx.secrets.KEY
npx w3f test with userArgs.json thyme run <task> with args.json
npx w3f deploy, then create a task from the CID thyme upload <task> --tag v1, then create an executable
Time interval and cron triggers Interval (30 seconds to one day) and cron
On-chain event or every-block triggers An interval that polls, with a block cursor in ctx.storage
Dedicated msg.sender Your own Safe, through a role limited to the calls you allow

Where it is different

  • Calls come from a Safe you own. Gelato sent calls from its dedicated msg.sender. Flow sends them through a Zodiac Roles role on your Safe, limited to the contracts and functions you approve. Remove the role and access is gone. If your contract only accepted Gelato's sender, allow the Safe instead.
  • No event or block triggers. Poll on a short interval and return canExec: false when there is nothing new. Skipped runs send nothing.
  • Gas in USD. Mainnet gas is sponsored and charged to a prepaid USD balance. Testnet gas is free.

Let your coding agent do it

If you use Claude Code, Codex, Cursor or a similar agent, paste this into it from the repository that holds your Gelato functions:

Read https://flow.thymelabs.io/docs/skills/thyme-migrate-gelato/SKILL.md and follow it to port the Gelato Web3 Functions in this repository to Thyme Flow. If you support skills, save that file as a skill named thyme-migrate-gelato. Start by listing the functions and triggers you find, port one, run it locally, and ask me before uploading or scheduling anything.
Enter fullscreen mode Exit fullscreen mode

The skill tells the agent to inventory first, port one function at a time, test each locally, and stop on anything that needs you: a contract that only accepts Gelato, fee transfers to Gelato, calls that send ETH, or a chain that is not enabled. Login approval and Safe permissions always stay with you.

Coming from Chainlink Automation instead?

Chainlink retired Automation v2.1 on July 31, 2026. If your contract implements checkUpkeep and performUpkeep, one small task can call them unchanged. The guide is here: Chainlink Automation to Thyme Flow.

Links

Stuck on a port? Send the function to [email protected] and we'll help you move it.

Top comments (0)