# Onchain Write
Source: https://docs.chain.link/cre/guides/workflow/using-evm-client/onchain-write/overview-ts
Last Updated: 2026-09-18

> For the complete documentation index, see [llms.txt](/llms.txt).

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

- [Understanding how CRE writes work](#understanding-how-cre-writes-work) - The secure write flow
- [What you need: A consumer contract](#what-you-need-a-consumer-contract) - Contract requirements
- [The TypeScript write process](#the-typescript-write-process) - Two-step approach overview
- [Next steps](#next-steps) - Where to go from here

## 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:

| Destination                    | Guide                                                                                                                       | Verification                                                                                                           |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Smart contract (via Forwarder) | This section + [Submitting Reports Onchain](/cre/guides/workflow/using-evm-client/onchain-write/submitting-reports-onchain) | Onchain in `KeystoneForwarder`                                                                                         |
| HTTP API                       | [Submitting Reports via HTTP](/cre/guides/workflow/using-http-client/submitting-reports-http-ts)                            | [Verifying CRE Reports Offchain](/cre/guides/workflow/using-http-client/verifying-reports-offchain-ts) on the receiver |

See [API Interactions: CRE reports over HTTP](/cre/guides/workflow/using-http-client#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](/cre/guides/workflow/using-evm-client/onchain-write/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 <a href="https://viem.sh/docs/abi/encodeAbiParameters" target="_blank">viem's `encodeAbiParameters()`</a>
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.

> **NOTE: Already familiar with the Getting Started tutorial?**
>
> The approach covered in [Part 4: Writing Onchain](/cre/getting-started/part-4-writing-onchain) uses this same two-step
> pattern. This section provides the conceptual foundation for that tutorial.

## 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`](/cre/reference/sdk/evm-client-ts#writereportreply) with two status fields you should check:

| Field                             | Type                              | Meaning                                                                                                  |
| --------------------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `txStatus`                        | `TxStatus`                        | Whether the transaction itself succeeded: `SUCCESS`, `REVERTED`, or `FATAL`.                             |
| `receiverContractExecutionStatus` | `ReceiverContractExecutionStatus` | Whether your consumer contract's `onReport()` executed successfully: `SUCCESS` or `REVERTED` (optional). |
| `txHash`                          | `Uint8Array`                      | The 32-byte transaction hash, useful for looking up the transaction on a block explorer.                 |
| `errorMessage`                    | `string`                          | An 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:

```typescript
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:

```typescript
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)
}
```

> **CAUTION: Replay attacks on retry**
>
> A reverted report is **not** marked as used by the forwarder, so retrying (or anyone else) can resubmit the same signed report once conditions change. If your workflow takes corrective action after a failure, embed a monotonic execution timestamp in the report payload and reject stale reports in your consumer contract. See [Same-chain replay on failure](/cre/guides/workflow/using-evm-client/onchain-write/building-consumer-contracts#same-chain-replay-on-failure).

### 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](https://app.chain.link/cre/workflows).

**Workflow-level inspection:**

```bash
# 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:**

```bash
# 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:**

```bash
# 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
```

> **NOTE: Simulation vs production**
>
> `cre workflow simulate` uses a **MockForwarder** that records the report but does **not** call your consumer contract's `onReport()`. As a result, `receiverContractExecutionStatus` is always `SUCCESS` in simulation. To observe real `onReport()` reverts (and the retry behavior above), deploy the workflow with `cre workflow deploy` and inspect its executions with the CLI commands above.

## Next steps

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

1. **[Building Consumer Contracts](/cre/guides/workflow/using-evm-client/onchain-write/building-consumer-contracts)** - Create a Solidity contract to receive your workflow's data
2. **[Writing Data Onchain](/cre/guides/workflow/using-evm-client/onchain-write/writing-data-onchain)** - Complete step-by-step guide with examples for single values and structs

**Additional resources:**

- **[EVM Client Reference](/cre/reference/sdk/evm-client-ts)** - Complete API documentation
- **[Onchain Read](/cre/guides/workflow/using-evm-client/onchain-read-ts)** - Reading data from smart contracts