# Part 4: Writing Onchain
Source: https://docs.chain.link/cre/getting-started/part-4-writing-onchain-ts
Last Updated: 2025-12-09


In the previous parts, you successfully fetched offchain data and read from a smart contract. Now, you'll complete the "Onchain Calculator" by writing your computed result back to the blockchain.

## What you'll do

- Use the `CalculatorConsumer` contract to receive workflow results
- Modify your workflow to write data to the blockchain using the EVM capability
- Execute your first onchain write transaction through CRE
- Verify your result on the blockchain

## Step 1: The consumer contract

To write data onchain, your workflow needs a target smart contract (a "consumer contract"). For this guide, we have pre-deployed a simple `CalculatorConsumer` contract on the Sepolia testnet. This contract is designed to receive and store the calculation results from your workflow.

Here is the source code for the contract so you can see how it works:

> **NOTE: Note**
>
> Don't worry if you don't understand every line of this contract right now. We're showing it to you for context, but
> the key takeaway is that it's designed to securely receive data from a CRE workflow. We'll cover the important details
> of how this works in a later guide.

```sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {ReceiverTemplate} from "./ReceiverTemplate.sol";

/**
 * @title CalculatorConsumer (Testing Version)
 * @notice This contract receives reports from a CRE workflow and stores the results of a calculation onchain.
 * @dev Inherits from ReceiverTemplate which provides security checks. The forwarder address must be
 * configured at deployment. Additional security checks (workflowId, workflowName, author) can be enabled via setter
 * functions.
 */
contract CalculatorConsumer is ReceiverTemplate {
  // Struct to hold the data sent in a report from the workflow
  struct CalculatorResult {
    uint256 offchainValue;
    int256 onchainValue;
    uint256 finalResult;
  }

  // --- State Variables ---
  CalculatorResult public latestResult;
  uint256 public resultCount;
  mapping(uint256 => CalculatorResult) public results;

  // --- Events ---
  event ResultUpdated(uint256 indexed resultId, uint256 finalResult);

  /**
   * @notice Constructor requires the forwarder address for security
   * @param _forwarderAddress The address of the Chainlink Forwarder contract (for testing: MockForwarder)
   * @dev The forwarder address enables the first layer of security - only the forwarder can call onReport.
   * Additional security checks can be configured after deployment using setter functions.
   */
  constructor(
    address _forwarderAddress
  ) ReceiverTemplate(_forwarderAddress) {}

  /**
   * @notice Implements the core business logic for processing reports.
   * @dev This is called automatically by ReceiverTemplate's onReport function after security checks.
   */
  function _processReport(
    bytes calldata report
  ) internal override {
    // Decode the report bytes into our CalculatorResult struct
    CalculatorResult memory calculatorResult = abi.decode(report, (CalculatorResult));

    // --- Core Logic ---
    // Update contract state with the new result
    resultCount++;
    results[resultCount] = calculatorResult;
    latestResult = calculatorResult;

    emit ResultUpdated(resultCount, calculatorResult.finalResult);
  }

  // This function is a "dry-run" utility. It allows an offchain system to check
  // if a prospective result is an outlier before submitting it for a real onchain update.
  // It is also used to guide the binding generator to create a method that accepts the CalculatorResult struct.
  function isResultAnomalous(
    CalculatorResult memory _prospectiveResult
  ) public view returns (bool) {
    // A result is not considered anomalous if it's the first one.
    if (resultCount == 0) {
      return false;
    }

    // Business logic: Define an anomaly as a new result that is more than double the previous result.
    // This is just one example of a validation rule you could implement.
    return _prospectiveResult.finalResult > (latestResult.finalResult * 2);
  }
}
```

The contract is already deployed for you on Sepolia at the following address: <a href="https://sepolia.etherscan.io/address/0x95e10BaC2B89aB4D8508ccEC3f08494FcB3D23cb#code" target="_blank" rel="noopener noreferrer">`0x95e10BaC2B89aB4D8508ccEC3f08494FcB3D23cb`</a>. You will use this address in your configuration file.

## Step 2: Update your workflow configuration

Add the `CalculatorConsumer` contract address to your `config.staging.json`:

```json
{
  "schedule": "*/30 * * * * *",
  "apiUrl": "https://api.mathjs.org/v4/?expr=randomInt(1,101)",
  "evms": [
    {
      "storageAddress": "0xa17CF997C28FF154eDBae1422e6a50BeF23927F4",
      "calculatorConsumerAddress": "0x95e10BaC2B89aB4D8508ccEC3f08494FcB3D23cb",
      "chainName": "ethereum-testnet-sepolia",
      "gasLimit": "500000"
    }
  ]
}
```

## Step 3: Update your workflow logic

Now modify your workflow to write the final result to the contract. Writing onchain involves a two-step process:

1. **Generate a signed report**: Use `runtime.report()` to create a cryptographically signed report from your workflow data
2. **Submit the report**: Use `evmClient.writeReport()` to submit the signed report to the consumer contract

The TypeScript SDK uses Viem's `encodeAbiParameters` to properly encode the struct data according to the contract's ABI before generating the report.

> **NOTE: Configuring Gas Limit**
>
> Notice that in the code below, we pass the `gasLimit` from our config file to the `writeReport` function. Explicitly
> setting a sufficient gas limit is crucial for write operations to prevent them from failing due to "out of gas"
> errors.

Replace the entire content of `onchain-calculator/my-calculator-workflow/main.ts` with this final version.

**Note:** Lines highlighted in green indicate new or modified code compared to Part 3.

Code snippet for onchain-calculator/my-calculator-workflow/main.ts:

```typescript
import {
  CronCapability,
  HTTPClient,
  EVMClient,
  handler,
  consensusMedianAggregation,
  Runner,
  type NodeRuntime,
  type Runtime,
  getNetwork,
  LAST_FINALIZED_BLOCK_NUMBER,
  encodeCallMsg,
  bytesToHex,
  hexToBase64,
} from "@chainlink/cre-sdk"
import { encodeAbiParameters, parseAbiParameters, encodeFunctionData, decodeFunctionResult, zeroAddress } from "viem"
import { Storage } from "../contracts/abi"

type EvmConfig = {
  chainName: string
  storageAddress: string
  calculatorConsumerAddress: string
  gasLimit: string
}

type Config = {
  schedule: string
  apiUrl: string
  evms: EvmConfig[]
}


// MyResult struct now holds all the outputs of our workflow.
type MyResult = {
  offchainValue: bigint
  onchainValue: bigint
  finalResult: bigint
  txHash: string
}


const initWorkflow = (config: Config) => {
  const cron = new CronCapability()

  return [handler(cron.trigger({ schedule: config.schedule }), onCronTrigger)]
}

const onCronTrigger = (runtime: Runtime<Config>): MyResult => {
  const evmConfig = runtime.config.evms[0]

  // Convert the human-readable chain name to a chain selector
  const network = getNetwork({
    chainFamily: "evm",
    chainSelectorName: evmConfig.chainName,
  })
  if (!network) {
    throw new Error(`Unknown chain name: ${evmConfig.chainName}`)
  }

  // Step 1: Fetch offchain data
  const offchainValue = runtime.runInNodeMode(fetchMathResult, consensusMedianAggregation())().result()

  runtime.log(`Successfully fetched offchain value: ${offchainValue}`)

  // Step 2: Read onchain data using the EVM client
  const evmClient = new EVMClient(network.chainSelector.selector)

  const callData = encodeFunctionData({
    abi: Storage,
    functionName: "get",
  })

  const contractCall = evmClient
    .callContract(runtime, {
      call: encodeCallMsg({
        from: zeroAddress,
        to: evmConfig.storageAddress as `0x${string}`,
        data: callData,
      }),
      blockNumber: LAST_FINALIZED_BLOCK_NUMBER,
    })
    .result()

  const onchainValue = decodeFunctionResult({
    abi: Storage,
    functionName: "get",
    data: bytesToHex(contractCall.data),
  }) as bigint

  runtime.log(`Successfully read onchain value: ${onchainValue}`)


  // Step 3: Calculate the final result
  const finalResultValue = onchainValue + offchainValue

  runtime.log(`Final calculated result: ${finalResultValue}`)

  // Step 4: Write the result to the consumer contract
  const txHash = updateCalculatorResult(
    runtime,
    network.chainSelector.selector,
    evmConfig,
    offchainValue,
    onchainValue,
    finalResultValue
  )

  // Step 5: Log and return the final, consolidated result.
  const finalWorkflowResult: MyResult = {
    offchainValue,
    onchainValue,
    finalResult: finalResultValue,
    txHash,
  }

  runtime.log(
    `Workflow finished successfully! offchainValue: ${offchainValue}, onchainValue: ${onchainValue}, finalResult: ${finalResultValue}, txHash: ${txHash}`
  )

  return finalWorkflowResult
}


const fetchMathResult = (nodeRuntime: NodeRuntime<Config>): bigint => {
  const httpClient = new HTTPClient()

  const req = {
    url: nodeRuntime.config.apiUrl,
    method: "GET" as const,
  }

  const resp = httpClient.sendRequest(nodeRuntime, req).result()
  const bodyText = new TextDecoder().decode(resp.body)
  const val = BigInt(bodyText.trim())

  return val
}


// updateCalculatorResult handles the logic for writing data to the CalculatorConsumer contract.
function updateCalculatorResult(
  runtime: Runtime<Config>,
  chainSelector: bigint,
  evmConfig: EvmConfig,
  offchainValue: bigint,
  onchainValue: bigint,
  finalResult: bigint
): string {
  runtime.log(`Updating calculator result for consumer: ${evmConfig.calculatorConsumerAddress}`)

  const evmClient = new EVMClient(chainSelector)

  // Encode the CalculatorResult struct according to the contract's ABI
  const reportData = encodeAbiParameters(
    parseAbiParameters("uint256 offchainValue, int256 onchainValue, uint256 finalResult"),
    [offchainValue, onchainValue, finalResult]
  )

  runtime.log(
    `Writing report to consumer contract - offchainValue: ${offchainValue}, onchainValue: ${onchainValue}, finalResult: ${finalResult}`
  )

  // Step 1: Generate a signed report using the consensus capability
  const reportResponse = runtime
    .report({
      encodedPayload: hexToBase64(reportData),
      encoderName: "evm",
      signingAlgo: "ecdsa",
      hashingAlgo: "keccak256",
    })
    .result()

  // Step 2: Submit the report to the consumer contract
  const writeReportResult = evmClient
    .writeReport(runtime, {
      receiver: evmConfig.calculatorConsumerAddress,
      report: reportResponse,
      gasConfig: {
        gasLimit: evmConfig.gasLimit,
      },
    })
    .result()

  runtime.log("Waiting for write report response")

  const txHash = bytesToHex(writeReportResult.txHash || new Uint8Array(32))
  runtime.log(`Write report transaction succeeded: ${txHash}`)
  runtime.log(`View transaction at https://sepolia.etherscan.io/tx/${txHash}`)
  return txHash
}


export async function main() {
  const runner = await Runner.newRunner<Config>()
  await runner.run(initWorkflow)
}
```

> **NOTE: Optional: Add runtime validation**
>
> If you added Zod validation in Part 1, you can continue using it here. The `Config` type now includes `gasLimit` and `calculatorConsumerAddress` fields, so update your Zod schema accordingly. See [Part 1: Optional Runtime Validation](/cre/getting-started/part-1-project-setup#optional-runtime-validation) for details.

**Key TypeScript SDK features for writing:**

- **`encodeAbiParameters()`**: From Viem, encodes structured data according to a contract's ABI
- **`parseAbiParameters()`**: From Viem, defines the parameter types for encoding
- **`runtime.report()`**: Generates a signed report using the consensus capability
- **`writeReport()`**: EVMClient method for submitting the signed report to a consumer contract
- **`txHash`**: The transaction hash returned after a successful write operation

> **TIP: Convenience helpers for cleaner code**
>
> Once you're comfortable with these core concepts, the SDK provides convenience helpers for more concise code:

- **HTTP helpers**: The [`HTTPSendRequester`](/cre/reference/sdk/http-client-ts#using-sendrequester) type includes [`ok()`](/cre/reference/sdk/http-client-ts#ok), [`text()`](/cre/reference/sdk/http-client-ts#text), and [`json()`](/cre/reference/sdk/http-client-ts#json) functions for cleaner response handling
- **Report helper**: [`prepareReportRequest()`](/cre/reference/sdk/evm-client-ts#preparereportrequest) automatically sets default encoding parameters (`ecdsa`, `keccak256`, `evm`) for report generation

These helpers are great for production code, but we use the explicit approach here for educational clarity.

> **NOTE: Organizing ABIs for reusability**
>
> Notice that we encoded the `CalculatorResult` struct inline using `parseAbiParameters()`. This works great for one-off
> workflows. However, if you're building workflows that interact with the same consumer contract multiple times or want
> better code organization, you can define the ABI parameters in a dedicated file (like we did with the `Storage`
> contract in [Part 3](/cre/getting-started/part-3-reading-onchain-value-ts#step-3-create-the-contract-abi-file)). To
> learn more about organizing ABIs for write operations, see [Organizing ABIs for reusable data
> structures](/cre/guides/workflow/using-evm-client/onchain-write/writing-data-onchain#organizing-abis-for-reusable-data-structures).

## Step 4: Run the simulation and review the output

> **NOTE: Funding Your Account**
>
> This step submits an onchain transaction, which requires gas. Before running the simulation, verify that the account
> associated with the private key from [Part
> 1](/cre/getting-started/part-1-project-setup-ts#set-up-your-private-key) is funded with sufficient Sepolia ETH.
> An unfunded account will cause the transaction to fail, often with an error message like `gas required exceeds
>   allowance`.

If you need more Sepolia ETH, go to <a href="https://faucets.chain.link" target="blank">faucets.chain.link</a> to get some Sepolia ETH.

> **CAUTION: Broadcasting Your Transaction**
>
> By default, `cre workflow simulate` performs a **dry run** for onchain write operations. It will simulate the transaction and return a successful response, but will **not** broadcast it to the network, resulting in an empty transaction hash (`0x`).

To execute a real transaction, you must add the `--broadcast` flag to the command.

Run the simulation from your project root directory (the `onchain-calculator/` folder). Because there is only one trigger, the simulator runs it automatically.

```bash
cre workflow simulate my-calculator-workflow --target staging-settings --broadcast
```

Your workflow will now show the complete end-to-end execution, including the final log of the `MyResult` object containing the transaction hash.

```bash
Workflow compiled
2026-01-09T17:52:05Z [SIMULATION] Simulator Initialized

2026-01-09T17:52:05Z [SIMULATION] Running trigger trigger=cron-trigger@1.0.0
2026-01-09T17:52:06Z [USER LOG] Successfully fetched offchain value: 68
2026-01-09T17:52:06Z [USER LOG] Successfully read onchain value: 22
2026-01-09T17:52:06Z [USER LOG] Final calculated result: 90
2026-01-09T17:52:06Z [USER LOG] Updating calculator result for consumer: 0x95e10BaC2B89aB4D8508ccEC3f08494FcB3D23cb
2026-01-09T17:52:06Z [USER LOG] Writing report to consumer contract - offchainValue: 68, onchainValue: 22, finalResult: 90
2026-01-09T17:52:12Z [USER LOG] Waiting for write report response
2026-01-09T17:52:12Z [USER LOG] Write report transaction succeeded: 0x6346d9eeca2f2875131d38aa9903a216f16e3cc7188f0ac6a1d5cd1fcbfbf9e6
2026-01-09T17:52:12Z [USER LOG] View transaction at https://sepolia.etherscan.io/tx/0x6346d9eeca2f2875131d38aa9903a216f16e3cc7188f0ac6a1d5cd1fcbfbf9e6
2026-01-09T17:52:12Z [USER LOG] Workflow finished successfully! offchainValue: 68, onchainValue: 22, finalResult: 90, txHash: 0x6346d9eeca2f2875131d38aa9903a216f16e3cc7188f0ac6a1d5cd1fcbfbf9e6

Workflow Simulation Result:
 {
  "finalResult": 90,
  "offchainValue": 68,
  "onchainValue": 22,
  "txHash": "0x6346d9eeca2f2875131d38aa9903a216f16e3cc7188f0ac6a1d5cd1fcbfbf9e6"
}

2026-01-09T17:52:12Z [SIMULATION] Execution finished signal received
2026-01-09T17:52:12Z [SIMULATION] Skipping WorkflowEngineV2
```

- **`[USER LOG]`**: You can see all of your `logger.info()` calls showing the complete workflow execution, including the offchain value (`68`), onchain value (`22`), final calculation (`90`), and the transaction hash.
- **`[SIMULATION]`**: These are system-level messages from the simulator showing its internal state.
- **`Workflow Simulation Result`**: This is the final return value of your workflow. The `MyResult` object contains all the values (68 + 22 = 90) and the transaction hash confirming the write operation succeeded.

## Step 5: Verify the result onchain

### **1. Check the Transaction**

In your terminal output, you'll see a clickable URL to view the transaction on Sepolia Etherscan:

```
[USER LOG] View transaction at https://sepolia.etherscan.io/tx/0x...
```

Click the URL (or copy and paste it into your browser) to see the full details of the transaction your workflow submitted.

**What are you seeing on a blockchain explorer?**

You'll notice the transaction's `to` address is not the `CalculatorConsumer` contract you intended to call. Instead, it's to a **Forwarder** contract. Your workflow sends a secure report to the Forwarder, which then verifies the request and makes the final call to the `CalculatorConsumer` on your workflow's behalf. To learn more, see the [Onchain Write guide](/cre/guides/workflow/using-evm-client/onchain-write/overview).

### **2. Check the contract state**

While your wallet interacted with the Forwarder, the `CalculatorConsumer` contract's state was still updated. You can verify this change directly on Etherscan:

- Navigate to the `CalculatorConsumer` contract address: <a href="https://sepolia.etherscan.io/address/0x95e10BaC2B89aB4D8508ccEC3f08494FcB3D23cb#readContract" target="_blank" rel="noopener noreferrer">`0x95e10BaC2B89aB4D8508ccEC3f08494FcB3D23cb`</a>.
- Expand the `latestResult` function and click **Query**. The values should match the `finalResult`, `offchainValue`, and `onchainValue` from your workflow logs.

This completes the end-to-end loop: triggering a workflow, fetching data, reading onchain state, and verifiably writing the result back to a public blockchain.

To learn more about implementing consumer contracts and the secure write process, see these guides:

- **[Building Consumer Contracts](/cre/guides/workflow/using-evm-client/onchain-write/building-consumer-contracts)**: Learn how to create your own secure consumer contracts with proper validation.
- **[Onchain Write Guide](/cre/guides/workflow/using-evm-client/onchain-write/overview-ts)**: Dive deeper into the write patterns.

## Next steps

You've now mastered the complete CRE development workflow!

- **[Before You Build](/cre/getting-started/before-you-build-ts)**: Don't skip this — critical tips before building your own workflows.