Onchain Write

This overview explains how writing data onchain works in CRE and how the TypeScript SDK handles it.

Understanding how CRE writes work

Before diving into code, it's important to understand how CRE handles onchain writes differently than traditional web3 applications.

Why CRE doesn't write directly to your contract

In a traditional web3 app, you'd create a transaction and send it directly to your smart contract. CRE uses a different, more secure approach for three key reasons:

  1. Decentralization: Multiple nodes in the Decentralized Oracle Network (DON) need to agree on what data to write
  2. Verification: The blockchain needs cryptographic proof that the data came from a trusted Chainlink network
  3. Accountability: There must be a verifiable trail showing which workflow and owner created the data

The secure write flow (4 steps)

Here's the journey your workflow's data takes to reach the blockchain:

  1. Report generation: Your workflow generates a report—your data is ABI-encoded and wrapped in a cryptographically signed "package"
  2. DON consensus: The DON reaches consensus on the report's contents
  3. Forwarder submission: A designated node submits the report to a Chainlink KeystoneForwarder contract
  4. Delivery to your contract: The Forwarder validates the report's signatures and calls your consumer contract's onReport() function with the data

In your workflow code, this process involves two steps: calling runtime.report() to generate the signed report, then calling evmClient.writeReport() to submit it to the blockchain.

Where reports can go after generation

The same signed report from runtime.report() can be delivered in different ways:

DestinationGuideVerification
Smart contract (via Forwarder)This section + Submitting Reports OnchainOnchain in KeystoneForwarder
HTTP APISubmitting Reports via HTTPVerifying CRE Reports Offchain on the receiver

See API Interactions: CRE reports over HTTP for the sender → receiver flow.

What you need: A consumer contract

Before you can write data onchain, you need a consumer contract. This is the smart contract that will receive your workflow's data.

What is a consumer contract?

A consumer contract is your smart contract that implements the IReceiver interface. This interface defines an onReport() function that the Chainlink Forwarder calls to deliver your workflow's data.

Think of it as a mailbox that's designed to receive packages (reports) from Chainlink's secure delivery service (the Forwarder contract).

Key requirement:

Your contract must implement the IReceiver interface. This single requirement ensures your contract has the necessary onReport(bytes metadata, bytes report) function that the Chainlink Forwarder calls to deliver data.

Getting started:

  • Don't have a consumer contract yet? Follow the Building Consumer Contracts guide to create one.
  • Already have one deployed? Great! Make sure you have its address and ABI ready for encoding your data.

The TypeScript write process

The TypeScript SDK uses a simple, two-step process for writing data onchain:

Step 1: Generate a signed report

Use runtime.report() to:

  1. ABI-encode your data using viem's encodeAbiParameters()
  2. Convert the encoded data to base64 format
  3. Generate a cryptographically signed report

Step 2: Submit the report

Use evmClient.writeReport() to submit the signed report to your consumer contract address.

Key features:

  • Use viem directly for ABI operations
  • Manual but flexible - Full control over encoding and submission
  • Type-safe - TypeScript and viem ensure compile-time safety
  • Works for any data - Single values, structs, arrays, etc.

Inspecting onchain transactions

When your workflow submits a report onchain, the transaction can fail in two distinct ways: the transaction itself can revert (for example, out of gas or an invalid receiver), or the transaction can succeed but your consumer contract's onReport() function can revert during execution. You should inspect both outcomes and decide how to respond.

Understanding the response

evmClient.writeReport() returns a WriteReportReply with two status fields you should check:

FieldTypeMeaning
txStatusTxStatusWhether the transaction itself succeeded: SUCCESS, REVERTED, or FATAL.
receiverContractExecutionStatusReceiverContractExecutionStatusWhether your consumer contract's onReport() executed successfully: SUCCESS or REVERTED (optional).
txHashUint8ArrayThe 32-byte transaction hash, useful for looking up the transaction on a block explorer.
errorMessagestringAn error message if the transaction failed.

Important: txStatus and receiverContractExecutionStatus are independent. A transaction can succeed (txStatus === TxStatus.SUCCESS) while the consumer contract's onReport() reverts (receiverContractExecutionStatus === ReceiverContractExecutionStatus.REVERTED). Always check both.

How to know if onReport() succeeded

The receiverContractExecutionStatus field tells you whether your consumer contract's onReport() function executed successfully. Check it after every write and log the result so you can monitor and troubleshoot deliveries:

import { EVMClient, TxStatus, bytesToHex, type Runtime } from "@chainlink/cre-sdk"
import { EVM_PB } from "@chainlink/cre-sdk/pb"

const writeResult = evmClient
  .writeReport(runtime, {
    receiver: config.consumerAddress,
    report: reportResponse,
    gasConfig: {
      gasLimit: config.gasLimit,
    },
  })
  .result()

// Always log the transaction hash and both statuses
const txHash = bytesToHex(writeResult.txHash || new Uint8Array(32))
runtime.log(
  `Write report response: txHash=${txHash} txStatus=${writeResult.txStatus} ` +
    `receiverStatus=${writeResult.receiverContractExecutionStatus}`
)

// Check the transaction status first
if (writeResult.txStatus !== TxStatus.SUCCESS) {
  throw new Error(`Transaction failed with status ${writeResult.txStatus}: ${writeResult.errorMessage}`)
}

// Then check whether onReport() executed successfully
if (writeResult.receiverContractExecutionStatus === EVM_PB.ReceiverContractExecutionStatus.REVERTED) {
  throw new Error(`onReport() reverted with status ${writeResult.receiverContractExecutionStatus}`)
}

Retry and reporting example

The following example shows a complete pattern for inspecting a write, logging the outcome, and retrying when the transaction or the consumer contract execution fails:

import { EVMClient, TxStatus, Report, bytesToHex, type Runtime } from "@chainlink/cre-sdk"
import { EVM_PB } from "@chainlink/cre-sdk/pb"

const MAX_RETRIES = 3

function submitReport(runtime: Runtime<unknown>, evmClient: EVMClient, report: Report, attempt = 0): void {
  const writeResult = evmClient
    .writeReport(runtime, {
      receiver: config.consumerAddress,
      report,
      gasConfig: {
        gasLimit: config.gasLimit,
      },
    })
    .result()

  const txHash = bytesToHex(writeResult.txHash || new Uint8Array(32))
  runtime.log(
    `Write report response: txHash=${txHash} txStatus=${writeResult.txStatus} ` +
      `receiverStatus=${writeResult.receiverContractExecutionStatus}`
  )

  // Retry on transaction failure
  if (writeResult.txStatus !== TxStatus.SUCCESS) {
    runtime.log(`Transaction failed, retrying: ${writeResult.errorMessage}`)
    retrySubmit(runtime, evmClient, report, attempt)
    return
  }

  // Retry if onReport() reverted even though the transaction succeeded
  if (writeResult.receiverContractExecutionStatus === EVM_PB.ReceiverContractExecutionStatus.REVERTED) {
    runtime.log(`onReport() reverted, retrying`)
    retrySubmit(runtime, evmClient, report, attempt)
    return
  }

  runtime.log(`Report delivered successfully: ${txHash}`)
}

function retrySubmit(runtime: Runtime<unknown>, evmClient: EVMClient, report: Report, attempt: number): void {
  if (attempt >= MAX_RETRIES) {
    throw new Error(`Report delivery failed after ${MAX_RETRIES} attempts`)
  }
  runtime.log(`Retrying submission (attempt ${attempt + 1}/${MAX_RETRIES})...`)
  // Add a delay here if your runtime supports it. Be careful about replay attacks — see below.
  submitReport(runtime, evmClient, report, attempt + 1)
}

Inspecting executions with the CLI

Once your workflow is deployed, you can inspect its executions programmatically with the CRE CLI. All commands support --output json for scripting. You can also view the same information in the CRE Workflows dashboard.

Workflow-level inspection:

# List all workflows for your organization
cre workflow list

# Deployment health + most recent execution for a workflow
cre workflow get ./my-workflow --target production-settings

Execution-level inspection:

# List executions (optionally filtered by workflow, status, or time range)
cre execution list evm-write-inspection
cre execution list evm-write-inspection --status FAILURE
cre execution list evm-write-inspection --limit 50 --output json

# Detailed status of a single execution (incl. top-level errors)
cre execution status <execution-uuid>

# Capability event timeline (per-event status, method, duration, errors)
cre execution events <execution-uuid>

# User-emitted log lines (e.g. your "Write report response" logs)
cre execution logs <execution-uuid>

Control commands:

# Pause / resume a workflow to stop or start trigger execution
cre workflow pause ./my-workflow --target production-settings --yes
cre workflow activate ./my-workflow --target production-settings --yes

Next steps

Now that you understand the concepts, follow these guides to implement onchain writes:

  1. Building Consumer Contracts - Create a Solidity contract to receive your workflow's data
  2. Writing Data Onchain - Complete step-by-step guide with examples for single values and structs

Additional resources:

Get the latest Chainlink content straight to your inbox.