Skip to content
Thyme Docs

ThymeContext (ctx)

ThymeContext is the one argument your run handler receives. It bundles everything a task needs: the execution account, parsed arguments, a read-only chain client, a captured logger, injected secrets, and persistent storage.

Handler signature

async run(ctx: ThymeContext<TArgs>): Promise<TaskResult>
interface ThymeContext<TArgs> {
  account: Address
  args: TArgs
  client: PublicClient
  logger: Logger
  secrets: Record<string, string>
  storage: JsonObject
}

TArgs is inferred from your schema by defineTask, so ctx.args is typed for free.

Fields

ctx.account

The viem Address that will execute calls returned by this task. This lets task logic read balances, allowances, roles, or other account-specific state without duplicating the executor address in ctx.args.

const balance = await ctx.client.getBalance({ address: ctx.account })

In the cloud, this is the address of the executable's selected profile. Locally, it is the checksummed value of SIMULATE_ACCOUNT. The field is also present in lifecycle callbacks.

ctx.args

The parsed and validated arguments, typed from your Zod schema. By the time run executes, values have already passed validation and transforms — for example, a z.address() field is a checksummed viem Address.

const { targetAddress, threshold } = ctx.args

Locally, args come from functions/<task>/args.json. In the cloud they come from the executable's args, rendered as a form from your schema. See Args.

ctx.client

A viem PublicClient for reads only: readContract, getBalance, getBlockNumber, getBlock, and so on.

const lastPrice = await ctx.client.readContract({
  address: oracleAddress,
  abi,
  functionName: 'getPrice',
})

The local client is lazy: a task that never accesses ctx.client can run without an RPC URL. The transport is built from RPC_URL. Locally that is whatever your .env points at; in the cloud it is derived from the executable's profile chain. See Chains.

ctx.logger

A Logger with info, warn, and error. Its output is captured and shown in the Console per-execution log view.

ctx.logger.info('Fetched price')
ctx.logger.warn('Price unchanged, skipping')

ctx.secrets

A Record<string, string> of secret values injected at runtime. Read by key:

const apiKey = ctx.secrets.PRICE_API_KEY

There is no explicit declaration API — using a key is the declaration. Locally, values come from your .env files (with reserved keys stripped); in the cloud they come from the secrets you bind to the executable. Cloud log redaction is a platform responsibility; the SDK logger itself does not redact values. Avoid printing secrets locally or in the cloud. See Secrets.

ctx.storage

A mutable JsonObject persisted across runs of the same executable. Mutate it in place:

ctx.storage.runs = ((ctx.storage.runs as number | undefined) ?? 0) + 1

Rules: plain JSON only, no undefined / NaN / Infinity / -0, forbidden keys __proto__ / constructor / prototype, and a 100 MiB cap. Locally it is seeded from storage.json and only written back with thyme run --persist. Successful persistence depends on the execution outcome; failed runs discard their pending writes. See Storage and callbacks.

Full example

functions/price-oracle/index.ts
import { defineTask, z } from '@thyme-labs/sdk'
import { encodeFunctionData } from 'viem'
 
const abi = [
  { name: 'getPrice', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
  { name: 'updatePrice', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'price', type: 'uint256' }], outputs: [] },
] as const
 
async function fetchPrice(url: string, apiKey: string): Promise<bigint> {
  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${apiKey}` },
  })
  if (!response.ok) throw new Error(`Price API returned ${response.status}`)
  const body = z.object({ priceWei: z.string().regex(/^[0-9]+$/) })
    .parse(await response.json())
  return BigInt(body.priceWei)
}
 
export default defineTask({
  schema: z.object({
    oracleAddress: z.address(),
    priceApiUrl: z.string().url(),
    threshold: z.coerce.bigint().positive(),
  }),
  async run(ctx) {
    const { oracleAddress, threshold } = ctx.args
 
    const lastPrice = await ctx.client.readContract({
      address: oracleAddress,
      abi,
      functionName: 'getPrice',
    })
 
    const apiKey = ctx.secrets.PRICE_API_KEY
    if (!apiKey) throw new Error('PRICE_API_KEY is required')
    const newPrice = await fetchPrice(ctx.args.priceApiUrl, apiKey)
 
    ctx.storage.runs = ((ctx.storage.runs as number | undefined) ?? 0) + 1
 
    if (newPrice > threshold && newPrice !== lastPrice) {
      return {
        canExec: true,
        calls: [{
          to: oracleAddress,
          data: encodeFunctionData({ abi, functionName: 'updatePrice', args: [newPrice] }),
        }],
      }
    }
    return { canExec: false, message: 'Price below threshold or unchanged' }
  },
})

The example expects a price endpoint returning { "priceWei": "123" } and a bound PRICE_API_KEY secret. Configure the endpoint, deployed oracle address, and threshold in task args, and declare updatePrice(uint256) for the oracle target in a permission manifest before releasing it.