Skip to content
Thyme Docs

Lifecycle Callbacks

Tasks may define onSkip, onError, onSuccess, and onFail alongside run. All receive the same typed context, including ctx.account, validated arguments, secrets, logger, and storage.

CallbackTriggerPayload
onSkip(ctx, info)run returns canExec: false. Runs inline.{ message: string }
onError(ctx, info)Task evaluation throws. Runs inline when the runtime can invoke it.{ error: string }
onSuccess(ctx, tx)Submitted calls are confirmed successfully. Runs in a separate callback phase.SuccessPayload
onFail(ctx, info)A post-submission attempt is recorded as failed, such as rejection or revert.FailPayload

Callbacks are best effort. A callback error is logged; it cannot turn an on-chain failure into a success or undo a confirmed transaction. They are not guaranteed delivery, and external side effects should tolerate retries or missing delivery. Startup errors can occur before any handler is entered.

Payload types

// Exported from @thyme-labs/sdk
type SuccessPayload = {
  txHash: string
  blockNumber: number
  gasUsed: string
  gasCostWei: string
  userOpHash?: string
}
 
type SkipPayload = { message: string }
type ErrorPayload = { error: string }
 
type FailPayload = {
  stage: 'reverted' | 'submit' | 'timeout'
  reason: string
  txHash?: string
  userOpHash?: string
}

Gas values are decimal strings. userOpHash is absent for raw transactions without an ERC-4337 UserOperation.

Failure stageMeaning
revertedThe receipt reports failure; txHash is available.
submitBroadcast or bundler rejection; this submission did not land.
timeoutConfirmation did not arrive in time. The transaction may still land; hashes are included when known.

A timeout is an unknown outcome. Check the known transaction or UserOperation before deciding whether another submission is appropriate. In the current cloud pipeline, a receipt timeout with a known transaction/UserOperation hash moves the execution to submitted_unknown and returns without invoking onFail. The SDK supports a timeout payload and the CLI can fabricate it, but not every cloud timeout invokes a failure callback.

Example

Add callbacks to a task that returns executable calls:

onSuccess(ctx, tx) {
  ctx.storage.lastTransaction = tx.txHash
  ctx.logger.info(`Confirmed in block ${tx.blockNumber}`)
},
onSkip(ctx, info) {
  ctx.logger.info(`Skipped: ${info.message}`)
},
onError(ctx, info) {
  ctx.logger.error(`Evaluation failed: ${info.error}`)
},
onFail(ctx, info) {
  ctx.logger.error(`${info.stage}: ${info.reason}`)
  if (info.stage === 'timeout') {
    ctx.logger.warn('Outcome unknown; check the transaction before retrying')
  }
},

Callbacks return void or Promise<void>. Await asynchronous work such as HTTP notifications; do not rely on background promises surviving runtime shutdown.

Storage and context lifetime

onSkip shares the current evaluation's storage object. A successful skipped run can persist those writes. A task error does not persist the failed run's writes, including writes made by onError.

Post-submission callbacks re-enter the task in a fresh runtime. onSuccess starts from committed successful-run storage; onFail starts from the previously committed state because the failed run's pending writes are discarded. Eligible callback storage writes are handled separately by the platform; a confirmed transaction remains confirmed even if a subsequent storage commit fails. See storage for commit and conflict behavior.

Keep information needed by later callbacks in args or storage, rather than module globals. Storage remains subject to JSON validation; an invalid storage mutation can still fail validation even when a callback exception is caught.

Local callback simulation

thyme run invokes onSkip and onError naturally. It never broadcasts transactions, so post-submission callbacks require fabricated outcomes:

thyme run my-task --callback onSuccess
thyme run my-task --callback onFail:reverted
thyme run my-task --callback onFail:submit
thyme run my-task --callback onFail:timeout

The task must return canExec: true; otherwise simulation is skipped. --callback implies --simulate-callbacks, whose interactive mode presents an outcome picker. Use --persist to write eligible produced storage to local storage.json. Fabricated hashes and receipts are not on-chain evidence.

Callback-name utilities

Import these from the package root or @thyme-labs/sdk/lifecycle:

import {
  LIFECYCLE_CALLBACK_NAMES,
  isLifecycleCallbackName,
  normalizeLifecycleCallbackNames,
  type LifecycleCallbackName,
} from '@thyme-labs/sdk/lifecycle'

LIFECYCLE_CALLBACK_NAMES is the readonly tuple ['onSuccess', 'onSkip', 'onError', 'onFail']. isLifecycleCallbackName(value: string) is a type guard. normalizeLifecycleCallbackNames(callbacks: readonly string[]) removes unknown names while preserving order and duplicates; it does not sort or deduplicate.