thyme/labs
Start building
Thyme Flow

Migrate from Gelato Web3 Functions

Web3 Functions, after Gelato.

Thyme Flow runs the same kind of job Gelato Web3 Functions did: a TypeScript function that reads the chain, decides, and returns the calls to send. Your decision logic carries over. The entry point, the provider and one field name change.

The same decision pattern
canExec
Shortest interval trigger
30 s
Sends every call
Your Safe

01 / Mapping

Every Gelato concept has a place.

Flow keeps the shape of a Web3 Function: arguments in, chain reads, a canExec decision and a list of calls out. This table maps each piece.

Gelato Web3 FunctionsThyme FlowNotes
Gelato Web3 FunctionsWeb3Function.onRun(async (context) => …)Thyme Flowexport default defineTask({ schema, async run(ctx) { … } })One module per task. Add onSuccess, onFail, onSkip and onError callbacks next to run if you need them.
Gelato Web3 FunctionsuserArgs declared in schema.jsonThyme Flowctx.args, typed by a Zod schemaArguments are validated before run. The console renders a form from the schema.
Gelato Web3 FunctionsmultiChainProvider.default() (ethers)Thyme Flowctx.client (a read-only viem client)Port new Contract(...).method() reads to ctx.client.readContract.
Gelato Web3 Functionsawait storage.get(key) and storage.set(key, value)Thyme Flowctx.storage.key = valuePlain JSON rather than strings only. Writes persist when the run succeeds.
Gelato Web3 Functionsawait secrets.get('KEY')Thyme Flowctx.secrets.KEYBind project secrets to the executable in the console.
Gelato Web3 Functions{ canExec: true, callData: [{ to, data }] }Thyme Flow{ canExec: true, calls: [{ to, data }] }Same descriptors, renamed field. Calls run in the order you return them.
Gelato Web3 Functions{ canExec: false, message }Thyme Flow{ canExec: false, message }Unchanged. The message shows up in the execution log.
Gelato Web3 Functionsnpx w3f test with userArgs.jsonThyme Flowthyme run <task> with args.jsonRuns locally against the RPC endpoint in your .env.
Gelato Web3 Functionsnpx w3f deploy, then create a task from the CIDThyme Flowthyme upload <task> --tag v1, then create an executableEach upload is an immutable release you can switch back to.
Gelato Web3 FunctionsTime interval and cron triggersThyme FlowInterval (30 seconds to one day) and cron triggersWebhooks can start extra runs with validated arguments.
Gelato Web3 FunctionsOn-chain event and every-block triggersThyme FlowAn interval trigger that pollsKeep a block cursor in ctx.storage and skip when nothing is new. Skipped runs send nothing.
Gelato Web3 FunctionsDedicated msg.senderThyme FlowYour own SafeFlow executes through a Zodiac Roles role limited to the contracts and functions you allow.

02 / Code

The same function, ported.

A price oracle that updates at most once an hour. The check, the API call and the call it returns stay the same. Only the reads and the return field change.

web3-functions/oracle/index.tsGelato
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' }
  }

  // fetchPrice is your existing API helper
  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]),
    }],
  }
})
functions/oracle/index.tsThyme 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' }
    }

    // fetchPrice is your existing API helper
    const price = await fetchPrice(ctx.secrets.PRICE_API_KEY)

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

03 / Steps

From CID to executable.

Port one function at a time. Run it locally first, then give it a schedule, so you can check each one before it sends anything on mainnet.

  1. 01

    Install the CLI and scaffold a task

    Create a project and one task per Web3 Function.

    npm install -g @thyme-labs/cli
    thyme init my-flow-project
    cd my-flow-project && npm install
    thyme new oracle
  2. 02

    Move the onRun body into run

    Swap the provider, arguments, storage and secrets using the table above, and return calls instead of callData. Copy the values from userArgs.json into args.json.

  3. 03

    Run it against the chain

    Set RPC_URL and SIMULATE_ACCOUNT in .env, then run the task. You see the decision and the calls it would send, without sending them.

    thyme run oracle
  4. 04

    Declare the calls it may send

    Add permissions.json next to the task. Flow rejects any returned call that is not declared before it is submitted.

    {
      "calls": [
        { "target": { "arg": "oracle" }, "function": "updatePrice(uint256)" }
      ]
    }
  5. 05

    Upload and schedule it

    Upload a release, set up a Safe profile that allows the same target and function, then create an executable with the interval or cron your Gelato task used.

    thyme upload oracle --tag v1
  6. 06

    Allow the Safe in your contract

    If your contract only accepted Gelato's dedicated msg.sender, add your Safe's address to the same allowlist. Then use Execute now in the console to confirm the first run.

04 / Differences

What works differently.

Most functions port directly. These are the places where Flow makes a different choice, so check them before you switch.

Calls come from a Safe you own

Gelato sent calls from its dedicated msg.sender. Flow sends them through a role on your own Safe, limited to the contracts and functions you approve. Remove the role from the Safe to revoke access.

No event or block triggers

Flow has no native log trigger. Poll on a short interval, keep the last block you processed in ctx.storage, and return canExec: false when there is nothing new.

Gas in USD, not 1Balance

Mainnet gas is sponsored and charged to the workspace dBank, a prepaid USD balance. Testnet gas is free.

viem instead of ethers

ctx.client is a read-only viem client and calldata is built with encodeFunctionData. Tasks never sign: they return calls and Flow submits them.

05 / FAQ

Questions

Is Gelato Web3 Functions still running?
No. Gelato ended Web3 Functions and Automate on March 31, 2026. Tasks created there no longer execute.
Do I have to rewrite my Web3 Function?
Not the logic. You change the entry point to defineTask, port ethers reads to ctx.client, and rename callData to calls. A single-file function is usually a short port.
Which chains does Flow support?
Mainnet automation runs on Polygon, OP Mainnet and BNB Smart Chain, and Ethereum Sepolia is available for testing. The console lists the networks enabled for your workspace. If you need another EVM chain, write to support@thymelabs.io.
Does Thyme hold the keys to my funds?
No owner key. With a Safe profile, Thyme runs an executor that is a member of one role on your Safe. That role only allows the contracts and functions you approve, and the executor cannot widen it or manage the Safe. You keep ownership and can remove the role at any time.
How much does Flow cost?
The Free plan covers testnets. Pro is $29 a month and Ultimate is $149 a month, both with mainnet access and more executables. Mainnet gas is sponsored and charged in USD to your workspace's prepaid dBank credit, so you do not keep a gas token on each chain.
Can you help port our functions?
Yes. Send the function to support@thymelabs.io and we will help you port it and check the first runs.

Start here

Bring the function. Keep the logic.

Port one Web3 Function, run it locally, and schedule it from your own Safe.