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.argsLocally, 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_KEYThere 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) + 1Rules: 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
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.
Related
- defineTask — how
ctx.argsgets its type. - Task Result Types — what to return.
- Local vs Cloud — how each field is sourced in each environment.