# Writing to Solana
Source: https://docs.chain.link/cre/guides/workflow/using-solana-client/onchain-write-ts
Last Updated: 2026-08-20

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

This guide walks through your first Solana write from a CRE workflow using the TypeScript SDK. The preferred path is
**generated bindings**: you place your Anchor IDL in the project, run `cre generate-bindings solana`, and call a typed
`writeReportFrom...` helper. The binding Borsh-encodes your payload, builds the forwarder report, and submits it — you
do not hand-roll encoding.

Onchain, the DON does not call your program directly. It submits a signed report to the **Keystone Forwarder** — a
Chainlink-managed Solana program that verifies DON signatures, then cross-program invokes (CPI) your receiver's
`on_report`. That is the same forwarder model as [EVM Write](/cre/capabilities/evm-read-write); see
[Solana Write](/cre/capabilities/solana-write) for the full flow. Local simulation uses a Devnet stand-in of that
forwarder so you can dry-run without deploying. After you deploy, the live forwarder addresses apply.

For IDL layout, generator output, and what each `writeReportFrom...` method does under the hood, see
[Generating Solana Bindings](/cre/guides/workflow/using-solana-client/generating-bindings-ts).

## Prerequisites

- A CRE project initialized with `cre init`. See [Part 1: Project Setup](/cre/getting-started/part-1-project-setup-ts) if you are starting from scratch.
- The latest version of the CRE CLI (`cre version` / `cre update`); **v1.29.0 or later** is required for TypeScript Solana binding generation.
- A funded Solana Devnet keypair for `CRE_SOLANA_PRIVATE_KEY` (required even for dry-run). Generate one with `solana-keygen new`, fund it with `solana airdrop` (or a transfer), and put the 64-byte base58 keypair in your project `.env`. See [Step 4](#step-4-run-the-simulation).

## What you need: a receiver program

Your Solana program must implement an `on_report` instruction that accepts the payload delivered by the forwarder. The
forwarder CPIs into your program after verifying the DON signatures. Your Anchor IDL's `types` section defines the
struct(s) you will write; the generator turns each into a `writeReportFrom<StructName>()` method.

This guide uses a minimal `cre_docs_receiver` program deployed to Solana Devnet at
`7k8NypziCPqVY8GYyCw7aqaR5aamFAMdH5ZaHVrZCS94`. Its `on_report` re-derives and verifies the `forwarder_authority` PDA,
then Borsh-decodes the report payload into a `UserData { key, value }` struct and logs it. It requires **no
receiver-specific accounts** beyond the two the forwarder always supplies (`state` and `forwarder_authority`). You can
simulate against it without deploying your own program. Swap in your IDL and call the generated helpers for your structs
when you are ready.

```rust
// cre_docs_receiver on_report (abridged)
pub fn on_report(ctx: Context<OnReport>, metadata: Vec<u8>, report: Vec<u8>) -> Result<()> {
    // ... re-derive + verify forwarder_authority PDA ...
    let user = UserData::deserialize(&mut &report[..])?; // Borsh { key, value }
    msg!("user.key={} user.value={}", user.key, user.value);
    Ok(())
}

#[derive(Accounts)]
pub struct OnReport<'info> {
    /// CHECK: forwarder state; its owner is the forwarder program.
    pub state: UncheckedAccount<'info>,
    /// PDA signer supplied by the forwarder CPI.
    pub forwarder_authority: Signer<'info>,
}
```

> **NOTE: How remainingAccounts maps to the forwarder**
>
> You provide `remainingAccounts` as `[forwarderState, forwarderAuthority, ...receiverAccounts]`. The **whole** list is
> hashed into the report (an integrity check the forwarder re-computes onchain). The Solana write capability then
> consumes indices 0 and 1 (`forwarderState`, `forwarderAuthority`) — it builds those into the forwarder instruction
> itself — and forwards only indices 2+ (your receiver-specific accounts) to your program's `on_report` CPI. So indices
> 0 and 1 must be present in the list you build (so the hash matches), even though the capability supplies them to the
> instruction directly. `cre_docs_receiver` has no receiver-specific accounts, so its list is just `[forwarderState,
>   forwarderAuthority]`.

## Step 1: Add your IDL and generate bindings

1. Save the following as `contracts/solana/src/idl/cre_docs_receiver.json` (or copy your own Anchor IDL into that
   folder). This is the deployed `cre_docs_receiver` program's IDL.

   ```json
   {
     "address": "7k8NypziCPqVY8GYyCw7aqaR5aamFAMdH5ZaHVrZCS94",
     "metadata": {
       "name": "cre_docs_receiver",
       "version": "0.1.0",
       "spec": "0.1.0",
       "description": "Receiver for cre-cli Solana forwarder simulation that logs a decoded UserData payload"
     },
     "instructions": [
       {
         "name": "on_report",
         "discriminator": [214, 173, 18, 221, 173, 148, 151, 208],
         "accounts": [
           { "name": "state" },
           {
             "name": "forwarder_authority",
             "docs": ["PDA signer supplied by the forwarder CPI."],
             "signer": true
           }
         ],
         "args": [
           { "name": "metadata", "type": "bytes" },
           { "name": "report", "type": "bytes" }
         ]
       }
     ],
     "events": [
       {
         "name": "ReceivedUserData",
         "discriminator": [154, 5, 186, 144, 208, 168, 118, 242]
       }
     ],
     "errors": [
       {
         "code": 6000,
         "name": "InvalidForwarderAuthority",
         "msg": "forwarder_authority is not the PDA for this state, receiver, and forwarder program"
       },
       {
         "code": 6001,
         "name": "InvalidUserData",
         "msg": "report payload could not be decoded as UserData"
       }
     ],
     "types": [
       {
         "name": "ReceivedUserData",
         "type": {
           "kind": "struct",
           "fields": [
             { "name": "forwarder", "type": "pubkey" },
             { "name": "forwarder_state", "type": "pubkey" },
             { "name": "forwarder_authority", "type": "pubkey" },
             { "name": "metadata_len", "type": "u32" },
             { "name": "report_len", "type": "u32" },
             { "name": "user", "type": { "defined": { "name": "UserData" } } }
           ]
         }
       },
       {
         "name": "UserData",
         "type": {
           "kind": "struct",
           "fields": [
             { "name": "key", "type": "string" },
             { "name": "value", "type": "string" }
           ]
         }
       }
     ]
   }
   ```

> **NOTE: Why UserData is referenced by the event**
>
> Anchor only emits a type into the IDL if it is referenced by an instruction argument, account, or event. Because
> `on_report` takes the payload as raw `bytes`, `UserData` would be dropped from the IDL — and the generator would not
> produce `writeReportFromUserData`. `cre_docs_receiver` references `UserData` in its `ReceivedUserData` event to keep
> it in the IDL. If your own program already takes a typed struct or exposes it via an account/event, no extra step is
> needed.

1. From your workflow directory, generate the bindings:

   ```bash
   cd my-workflow
   cre generate-bindings solana
   ```

   This creates `contracts/solana/ts/generated/CreDocsReceiver.ts` (plus a mock and `index.ts`). The `CreDocsReceiver`
   class includes `writeReportFromUserData(runtime, input, remainingAccounts, computeConfig?)` and exports the program
   ID as `CRE_DOCS_RECEIVER_PROGRAM_ID`.

   The generated bindings import from `@solana/codecs` and `@solana/addresses`. This workflow also imports
   `@solana/web3.js` (for the `forwarder_authority` PDA derivation) and `@solana/codecs-strings` (to base58-encode the
   returned transaction signature). If you run `tsc --noEmit` outside of `cre workflow simulate`, install them in your
   workflow folder: `npm install @solana/codecs @solana/addresses @solana/web3.js @solana/codecs-strings`.

## Step 2: Configure the workflow

1. Open (or create) `config.staging.json` and add the Solana Devnet simulation parameters.

   For `cre workflow simulate`, use these mock forwarder program and state accounts for Solana Devnet.
   `receiverProgramId` matches the IDL address above for this example; with your own program, use your Devnet program
   ID.

   ```json
   {
     "schedule": "*/30 * * * * *",
     "chainSelector": "16423721717087811551",
     "receiverProgramId": "7k8NypziCPqVY8GYyCw7aqaR5aamFAMdH5ZaHVrZCS94",
     "forwarderProgramId": "7kuEAA3mSC1Tz8gQjnvH7bKFda9xSPRRin9SZbH49cNK",
     "forwarderState": "5Tipz3yhTBdVsDbaBxZkrp7Gjf3brGq5SKkxReefPMP7"
   }
   ```

   | Field                | Description                                                                               |
   | -------------------- | ----------------------------------------------------------------------------------------- |
   | `chainSelector`      | Solana Devnet (`"16423721717087811551"`). Store as a string to avoid JSON precision loss. |
   | `receiverProgramId`  | Your receiver program (IDL address for this example)                                      |
   | `forwarderProgramId` | Forwarder program ID used for local simulation (fixed Devnet value)                       |
   | `forwarderState`     | Forwarder state account used for local simulation (fixed Devnet value)                    |

   The workflow derives `forwarderAuthority` at runtime from `["forwarder", forwarderState, receiverProgram]` under
   `forwarderProgramId`. When you deploy in [Step 5](#step-5-deploy-the-workflow), keep that derivation and point
   `forwarderProgramId` / `forwarderState` at the live forwarder addresses for your network.

### Simulation vs production forwarder addresses

The forwarder program and state accounts differ between local simulation and production. The config above uses the
**mock forwarder** addresses for `cre workflow simulate`. When you deploy to the DON, you must
replace them with the live Keystone Forwarder addresses for your network.

> **CAUTION: Important: Different addresses for simulation vs production**
>
> The mock forwarder program and state accounts used during simulation are **different** from the production Keystone
> Forwarder accounts used by deployed workflows. After testing with simulation, update `forwarderProgramId` and
> `forwarderState` in your production config to the live forwarder addresses for your network. See
> [Step 5](#step-5-deploy-the-workflow).

#### Simulation (mock forwarder)

These are the mock forwarder addresses for `cre workflow simulate`. They are the values shown in
the `config.staging.json` above:

| Network        | Mock Forwarder Program ID                    | Mock Forwarder State Account                 |
| -------------- | -------------------------------------------- | -------------------------------------------- |
| Solana Devnet  | 7kuEAA3mSC1Tz8gQjnvH7bKFda9xSPRRin9SZbH49cNK | 5Tipz3yhTBdVsDbaBxZkrp7Gjf3brGq5SKkxReefPMP7 |
| Solana Mainnet | 7kuEAA3mSC1Tz8gQjnvH7bKFda9xSPRRin9SZbH49cNK | jhCjuD4Z3V7HeSUChMRpkRwpw6B9yC63mxDMv8SdLNX  |

#### Production (Keystone Forwarder)

For production deployments, use these live Keystone Forwarder addresses:

| Network        | Keystone Forwarder Program ID                | Keystone Forwarder State Account             |
| -------------- | -------------------------------------------- | -------------------------------------------- |
| Solana Devnet  | CXsKEJcs25TQEYU2e5jZ8QTPE3ffMLZhH6BWHrdcCCB5 | 8QoomCQyPSkJ8WopJbX9B4HyvrFzziwvJdU8hZE6DCr9 |
| Solana Mainnet | GFrSSvQXaVGkc6Nrr8y2msie6pivJkR1s2EnDk4et294 | 9FgdPyU28bGMCuJyD34pzw9W7Ys36ziLbT9cbZtsaraV |

The workflow still derives `forwarderAuthority` from `["forwarder", forwarderState, receiverProgram]` under the production
`forwarderProgramId`.

## Step 3: Write the workflow

This step is a complete, pasteable workflow that performs one Solana write on a cron trigger using **generated
bindings**. It reads config, derives the forwarder authority PDA, builds the `remainingAccounts` list, and calls
`writeReportFromUserData()`. Replace `CreDocsReceiver` / `UserData` with your generated class and struct when you use
your own IDL.

1. Save the following as `my-workflow/main.ts`.

   ```typescript
   // my-workflow/main.ts
   import { CronCapability, handler, Runner, SolanaClient, solanaAccountMeta, type Runtime } from "@chainlink/cre-sdk"
   import { getBase58Decoder } from "@solana/codecs-strings"
   import { PublicKey } from "@solana/web3.js"
   import { CreDocsReceiver } from "./contracts/solana/ts/generated/CreDocsReceiver"

   type Config = {
     schedule: string
     chainSelector: string
     receiverProgramId: string
     forwarderState: string
     forwarderProgramId: string
   }

   // PDA seed literal used by the forwarder to derive the forwarder_authority.
   const FORWARDER_SEED = Uint8Array.from("forwarder", (_, i) => "forwarder".charCodeAt(i))

   // TxStatus / ReceiverContractExecutionStatus enum → readable labels.
   const TX_STATUS_LABELS: Record<number, string> = { 0: "FATAL", 1: "ABORTED", 2: "SUCCESS" }
   const RECEIVER_EXEC_LABELS: Record<number, string> = { 0: "SUCCESS", 1: "REVERTED" }

   const onCronTrigger = (runtime: Runtime<Config>) => {
     const config = runtime.config

     const client = new SolanaClient(BigInt(config.chainSelector))

     // Receiver program id comes from config here. The binding also exports the
     // program id baked in from the IDL `address` (CRE_DOCS_RECEIVER_PROGRAM_ID),
     // so you can use that instead of threading it through config if you prefer.
     const ds = new CreDocsReceiver(client, config.receiverProgramId)

     // Derive the forwarder_authority PDA the same way the on-chain forwarder /
     // receiver do: seeds = ["forwarder", forwarderState, receiverProgram],
     // program = forwarderProgram.
     const forwarderState = new PublicKey(config.forwarderState)
     const forwarderProgram = new PublicKey(config.forwarderProgramId)
     const receiverProgram = new PublicKey(config.receiverProgramId)
     const [forwarderAuthority] = PublicKey.findProgramAddressSync(
       [FORWARDER_SEED, forwarderState.toBytes(), receiverProgram.toBytes()],
       forwarderProgram
     )

     // remainingAccounts layout:
     //   index 0: forwarderState, index 1: forwarderAuthority, index 2+: receiver accounts.
     // The full list is hashed into the report; the Solana write capability consumes
     // indices 0/1 and forwards index 2+ to your program's on_report CPI.
     // cre_docs_receiver needs no receiver-specific accounts, so we pass just [state, authority].
     const remainingAccounts = [
       solanaAccountMeta(forwarderState.toBase58(), false),
       solanaAccountMeta(forwarderAuthority.toBase58(), false),
       // Append any receiver-specific accounts your program requires at index 2+, e.g.:
       // solanaAccountMeta(myWritableStatePda.toBase58(), true),
     ]

     runtime.log("Submitting UserData to Solana cre_docs_receiver program")

     // Solana simulate requires a non-nil computeConfig with computeLimit > 0
     const result = ds.writeReportFromUserData(runtime, { key: "price", value: "500000" }, remainingAccounts, {
       computeLimit: 200_000,
     })

     const hasSignature = !!result.txSignature && result.txSignature.length > 0
     const txStatusLabel = TX_STATUS_LABELS[result.txStatus] ?? `UNKNOWN(${result.txStatus})`
     const execStatusLabel =
       result.receiverContractExecutionStatus === undefined
         ? "(n/a)"
         : (RECEIVER_EXEC_LABELS[result.receiverContractExecutionStatus] ??
           `UNKNOWN(${result.receiverContractExecutionStatus})`)

     runtime.log("Solana write result:")
     runtime.log(`  txStatus:       ${txStatusLabel} (${result.txStatus})`)
     runtime.log(`  receiverStatus: ${execStatusLabel}`)
     runtime.log(`  transactionFee: ${result.transactionFee ?? 0n} lamports`)
     if (hasSignature) {
       const txHash = getBase58Decoder().decode(result.txSignature!)
       runtime.log(`  txHash:         ${txHash}`)
       runtime.log(`  explorer:       https://explorer.solana.com/tx/${txHash}?cluster=devnet`)
     } else {
       runtime.log("  txHash:         (not broadcast — simulation does not submit the transaction)")
     }
     if (result.errorMessage) {
       runtime.log(`  errorMessage:   ${result.errorMessage}`)
     }

     return result
   }

   export const initWorkflow = (config: Config) => {
     const cron = new CronCapability()
     return [handler(cron.trigger({ schedule: config.schedule }), onCronTrigger)]
   }

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

## Step 4: Run the simulation

Use this step to confirm compile, trigger, and the bindings write path with `cre workflow simulate`.

> **NOTE: Two separate YAML files**
>
> CRE uses two configuration files that live in different directories:

- **`workflow.yaml`** lives inside your workflow folder (`my-workflow/workflow.yaml`). It defines workflow metadata
  (name, registry) and points to your `main.ts` and config JSON.
- **`project.yaml`** lives at your **project root** (the directory you ran `cre init` in, one level above
  `my-workflow/`). It defines RPC endpoints and other project-level settings per target.

Both files use the same target names (e.g. `staging-settings`, `production-settings`) to link a workflow to its
RPCs. You will add to both files in this step.

1. Create a `workflow.yaml` in your workflow folder so the CLI can find your staging target.

   ```yaml
   staging-settings:
     user-workflow:
       workflow-name: "my-solana-write"
     workflow-artifacts:
       workflow-path: "./main.ts"
       config-path: "./config.staging.json"
       secrets-path: ""
   ```

2. Add a Solana RPC endpoint to your `project.yaml` (at your project root, **not** inside `my-workflow/`) under your
   simulation target.

   ```yaml
   staging-settings:
     rpcs:
       - chain-name: solana-devnet
         url: https://api.devnet.solana.com
   ```

3. Set `CRE_SOLANA_PRIVATE_KEY` in your project `.env`.

   Solana simulation requires a 64-byte base58 keypair even for dry-run. The transmitter account must exist and be
   funded on Solana Devnet. Generate a key with `solana-keygen new`, fund it with `solana airdrop` (or a transfer), and
   put the keypair in `.env`:

   ```bash
   CRE_SOLANA_PRIVATE_KEY=<your-64-byte-base58-keypair>
   ```

> **CAUTION: CRE\_ETH\_PRIVATE\_KEY is also required**
>
> The CRE CLI requires `CRE_ETH_PRIVATE_KEY` to be set even for Solana-only workflows — it is used for workflow
> owner derivation, not for the Solana transaction itself. Set any valid EVM private key in your `.env`:

```bash
CRE_ETH_PRIVATE_KEY=0x<your-evm-private-key>
```

Without it, `cre workflow simulate` and `cre workflow deploy` will fail with
`CRE_ETH_PRIVATE_KEY is not set`.

1. Run the dry-run simulation.

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

   Because `cre_docs_receiver` is deployed and the account list matches what the forwarder expects, the write completes
   successfully (`txStatus: SUCCESS`, `receiverStatus: SUCCESS`). Simulation does not broadcast the transaction here, so there
   is no `txSignature` / explorer link in the dry-run.

   ```
   ✓ Workflow compiled
   [SIMULATION] Simulator Initialized
   [SIMULATION] Running trigger trigger=cron-trigger@1.0.0
   [USER LOG] Submitting UserData to Solana cre_docs_receiver program
   [USER LOG] Solana write result:
   [USER LOG]   txStatus:       SUCCESS (2)
   [USER LOG]   receiverStatus: SUCCESS
   [USER LOG]   transactionFee: 15667 lamports
   [USER LOG]   txHash:         (not broadcast — simulation does not submit the transaction)

   ✓ Workflow Simulation Result:
   {
     "$typeName": "capabilities.blockchain.solana.v1alpha.WriteReportReply",
     "receiverContractExecutionStatus": 0,
     "transactionFee": 15667,
     "txStatus": 2
   }
   ```

> **NOTE: Troubleshooting write failures**
>
> `Custom:6002` (`InvalidAccountHash`) means the report's account hash did not match what the forwarder computed —
> usually because `forwarderProgramId` / `forwarderState` do not match the forwarder you are targeting, or the account
> list you built is wrong. `Custom:6000` (`InvalidForwarderAuthority`) means the derived `forwarder_authority` PDA is
> wrong for that state/receiver/forwarder combination. `Custom:6001` (`InvalidUserData`) means the receiver could not
> Borsh-decode the payload into `UserData`.

1. (Optional) Broadcast the transaction to Solana Devnet.

   By default, simulation dry-runs the write and returns no signature. Add `--broadcast` to actually submit the
   transaction through the mock forwarder. Your `CRE_SOLANA_PRIVATE_KEY` (transmitter) pays the fee, so it must be funded
   on Devnet.

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

   This time `WriteReport` returns a real `txSignature`, and the workflow logs a base58 `txHash` plus an explorer link:

   ```
   [USER LOG] Submitting UserData to Solana cre_docs_receiver program
   [USER LOG] Solana write result:
   [USER LOG]   txStatus:       SUCCESS (2)
   [USER LOG]   receiverStatus: SUCCESS
   [USER LOG]   transactionFee: 5000 lamports
   [USER LOG]   txHash:         c8ifuihaPd2erDuQvees1s2o8ikFmZuNF7WQE63PPGG2SXHZk5vXrHfe7nm4cj4fo7cE3Aq48ksMjP6ugPxAhqn
   [USER LOG]   explorer:       https://explorer.solana.com/tx/c8ifuihaPd2erDuQvees1s2o8ikFmZuNF7WQE63PPGG2SXHZk5vXrHfe7nm4cj4fo7cE3Aq48ksMjP6ugPxAhqn?cluster=devnet

   ✓ Workflow Simulation Result:
   {
     "$typeName": "capabilities.blockchain.solana.v1alpha.WriteReportReply",
     "receiverContractExecutionStatus": 0,
     "transactionFee": 5000,
     "txSignature": "Hkwkf/rL44luYyMfBIOJ9JgyLUUFUcMChfbqDTGcCND3zN9RziakjeWeywBkcjgXpYBRc23eVFM0BYeLcuMNBQ==",
     "txStatus": 2
   }
   ```

   Open the explorer link to confirm the transaction onchain: the mock forwarder is the top-level program and
   `cre_docs_receiver` appears as a CPI target. Note `txSignature` in the JSON result is base64-encoded; the workflow
   base58-decodes it into the `txHash` shown in the logs (the form Solana explorers expect).

   When you are ready to run on the DON with **your** receiver, continue to [Step 5](#step-5-deploy-the-workflow).

## Step 5: Deploy the workflow

A deployed workflow runs on the DON, which submits reports through the live Keystone Forwarder on Solana Devnet. Prefer
the [private registry](/cre/guides/operations/deploying-to-private-registry-ts) for this example (CRE login session; no
Ethereum gas for registry ops). You need [Deploy Access](/cre/account/deploy-access).

You can use the `cre_docs_receiver` example receiver as-is — it's deployed on Devnet and ready to accept writes. If you
have your own receiver program, replace `receiverProgramId` (and append any receiver-specific accounts in the workflow)
with your own values.

1. Create `config.production.json`. Keep your `receiverProgramId` and swap in the production Keystone Forwarder
   addresses:

   ```json
   {
     "schedule": "30 */5 * * * *",
     "chainSelector": "16423721717087811551",
     "receiverProgramId": "7k8NypziCPqVY8GYyCw7aqaR5aamFAMdH5ZaHVrZCS94",
     "forwarderProgramId": "CXsKEJcs25TQEYU2e5jZ8QTPE3ffMLZhH6BWHrdcCCB5",
     "forwarderState": "8QoomCQyPSkJ8WopJbX9B4HyvrFzziwvJdU8hZE6DCr9"
   }
   ```

   These are the Solana Devnet production Keystone Forwarder addresses. For Mainnet, see the
   [forwarder address table](#production-keystone-forwarder) above. If you are using your own receiver program, replace
   `receiverProgramId` with your own value. The workflow still derives `forwarderAuthority` from
   `["forwarder", forwarderState, receiverProgram]` under that `forwarderProgramId`.

2. Add a `production-settings` target to your existing `my-workflow/workflow.yaml` (append it below the
   `staging-settings` block you created in Step 4 — do not replace the staging block).

   ```yaml
   production-settings:
     user-workflow:
       workflow-name: "my-solana-write"
       deployment-registry: "private"
     workflow-artifacts:
       workflow-path: "./main.ts"
       config-path: "./config.production.json"
       secrets-path: ""
   ```

3. Add the Solana Devnet RPC under a `production-settings` block in your `project.yaml` (at your project root —
   append it below the existing `staging-settings` block). The URL can be the same as staging.

4. Deploy from your project root.

   ```bash
   cre workflow deploy my-workflow --target production-settings
   ```

   Expected shape:

   ```
   Deploying Workflow: my-solana-write
     Registry:      private
     ...
   ✓ Workflow registered in private registry
   ...
        Status:           Active
   ```

5. After the cron fires, check that the workflow executed successfully. You can view executions and logs in the
   [CRE platform](https://app.chain.link/cre/workflows), or use the CLI. List recent executions:

   ```bash
   cre execution list my-solana-write
   ```

   ```
   Executions

   1. 47517057f8ecb7455ddf9eb58d4a635e96bd968158146ed1973f9591cfdc1421
        Workflow:  my-solana-write
        Status:    SUCCESS
        Started:   2026-08-20 19:50:31 UTC
        Finished:  2026-08-20 19:50:43 UTC (12s)
   ```

6. View the execution logs to confirm the write reached Solana:

   ```bash
   cre execution logs <execution-id>
   ```

   Each DON node executes the workflow and logs the write result. A successful execution shows all nodes reaching
   `txStatus: SUCCESS` with a real on-chain transaction hash:

   ```
   [2026-08-20 19:50:31 UTC] [Node 1] Submitting UserData to Solana cre_docs_receiver program
   [2026-08-20 19:50:31 UTC] [Node 6] Submitting UserData to Solana cre_docs_receiver program
   ...
   [2026-08-20 19:50:43 UTC] [Node 1] Solana write result:
   [2026-08-20 19:50:43 UTC] [Node 1]   txStatus:       SUCCESS (2)
   [2026-08-20 19:50:43 UTC] [Node 1]   receiverStatus: (n/a)
   [2026-08-20 19:50:43 UTC] [Node 1]   transactionFee: 0 lamports
   [2026-08-20 19:50:43 UTC] [Node 1]   txHash:         2dJiCNgbGGhoMd7LSQH9FoUCWwUSzFyP8JHDzNrfcsGZcroSt1rsVPNzgq6LbMG8vjTzJPgeoGaViSABdeqa2v62
   [2026-08-20 19:50:43 UTC] [Node 1]   explorer:       https://explorer.solana.com/tx/2dJiCNgbGGhoMd7LSQH9FoUCWwUSzFyP8JHDzNrfcsGZcroSt1rsVPNzgq6LbMG8vjTzJPgeoGaViSABdeqa2v62?cluster=devnet
   ```

   Open the explorer link to confirm the transaction onchain: the Keystone Forwarder is the top-level program and
   `cre_docs_receiver` appears as a CPI target. All nodes share the same `txHash` because the DON reaches consensus on
   a single transaction.

   When you are done testing, pause the workflow to stop it from running:

   ```bash
   cre workflow pause my-workflow
   ```

See [Deploying to the Private Registry](/cre/guides/operations/deploying-to-private-registry-ts) for registry details, activate/pause, and CI flags.

## What happens onchain

When the deployed workflow runs on the DON:

1. The DON nodes each execute your workflow and reach consensus on the encoded payload.
2. A DON node calls the Keystone Forwarder program with the signed report and account list.
3. The forwarder verifies all DON signatures and re-computes the account hash over the full account list.
4. The forwarder supplies `forwarderState` + `forwarderAuthority` and CPIs into your program's `on_report` instruction
   with the decoded payload and your receiver-specific accounts (indices 2+).

The transaction on Solana Explorer will show the forwarder program as the initial callee, with your program appearing as a CPI target.

## Next steps

- **[Generating Solana Bindings](/cre/guides/workflow/using-solana-client/generating-bindings-ts)**: IDL layout, generator output, and what `writeReportFrom...` does
- **[Solana Client SDK Reference](/cre/reference/sdk/solana-client-ts)**: Full API reference for `SolanaClient` and helpers
- **[Solana Write Capability](/cre/capabilities/solana-write)**: Architecture and account layout reference