# Custom Rust Plugins
Source: https://docs.chain.link/cre/guides/operations/custom-rust-plugins-ts
Last Updated: 2026-04-20

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

TypeScript workflows run inside a [Javy](https://github.com/bytecodealliance/javy)-compiled <a href="https://webassembly.org/" target="_blank" rel="noopener noreferrer">WebAssembly</a> binary. By default, that binary includes the CRE SDK's built-in Rust plugin — pre-compiled to WASM and published with the SDK — which bridges your TypeScript to CRE node capabilities. You do not need [Rust](https://www.rust-lang.org/) installed to write and compile standard workflows.

Custom Rust plugins let you extend that plugin layer with your own Rust logic, exposing new functionality as typed globals your TypeScript workflow can call directly.

### How the two modes work

The SDK always produces a single compiled plugin WASM that is passed to [Javy](https://github.com/bytecodealliance/javy) at build time. When you include custom extensions, they are merged with the SDK's own Rust plugin code before that final WASM is produced. The mechanism for that merge differs depending on which mode you use:

- **`--plugin` (prebuilt)**: You provide a fully compiled `.plugin.wasm` that already contains all extensions merged together. No Rust toolchain required at workflow build time. Best for consuming a third-party Rust SDK (such as a ZK proof verifier) without managing a Rust compiler.
- **`--cre-exports` (source)**: You provide one or more Rust crate directories. The SDK compiles each crate, merges them with its own plugin code, and produces the final combined plugin at build time. Requires [Rust](https://www.rust-lang.org/) installed locally. Best for writing your own Rust logic or combining multiple extensions.

## When to use custom Rust plugins

Use a custom Rust plugin when you need logic that is impractical or impossible to express in TypeScript running inside QuickJS:

- **Cryptographic proof verification**: Verify ZK proofs, Risc-0 receipts, or other cryptographic attestations using Rust crates (e.g., `risc0-zkvm`) that have no JavaScript equivalent
- **Third-party Rust SDKs**: Integrate a Rust SDK that provides functionality not natively available in TypeScript — for example, a custom offchain report verifier published by a data provider
- **Performance-critical computation**: Rust running natively in WASM is significantly faster than interpreted JavaScript in QuickJS for compute-heavy operations like encoding, hashing, or numeric processing

> **NOTE: QuickJS limitations**
>
> Not all Node.js APIs are available in QuickJS. For example, `node:crypto` is unsupported. If a library you need
> doesn't work in simulation, a Rust plugin is the right escape hatch. See [TypeScript Runtime
> Environment](/cre/concepts/typescript-wasm-runtime) for details.

## Prerequisites

**Required versions:** TS SDK v1.6.0+

Before using custom Rust plugins, you need:

- **Bun** — the TypeScript SDK uses Bun for compilation. The Javy binary used by `cre-compile` is downloaded automatically when you run `bun install` via a `postinstall` hook — no manual setup required.
- **Custom WASM builds enabled** for your workflow — Rust plugins are injected via `cre-compile` flags, which you control from a custom `Makefile`. If your workflow still uses automatic compilation, convert it first:

  ```bash
  cre workflow custom-build ./my-workflow
  ```

  See [Custom WASM Builds](/cre/guides/operations/custom-build) for full details.

**Rust toolchain** — only if you compile your own extensions with `--cre-exports` (Mode 2) or follow the **Try it yourself** walkthrough below. You do **not** need Rust on your machine for **Mode 1 (`--plugin`)**: the prebuilt `.plugin.wasm` was already compiled upstream.

If you are using Mode 2 or the walkthrough, install a stable Rust toolchain and add the `wasm32-wasip1` target:

```bash
rustup target add wasm32-wasip1
```

## Mode 1: Prebuilt plugin (`--plugin`)

In prebuilt mode, you install an npm package that ships a compiled `.plugin.wasm`. Your workflow calls into it via a typed TypeScript accessor, and your `Makefile` passes the `.wasm` path to `cre-compile` with `--plugin`.

This is the simpler integration path — no Rust toolchain required at workflow build time.

### 1. Install the plugin package

The plugin author publishes their package to npm — install it like any other dependency:

```bash
bun add @acme/my-cre-plugin
```

The package ships a prebuilt `.plugin.wasm` alongside TypeScript types for the extension's API.

### 2. Import the accessor and call it from your workflow

A well-formed plugin package exports a typed accessor built with `createExtensionAccessor`. Import it directly and call it from your trigger handler:

```typescript
import { myExtension } from "@acme/my-cre-plugin"
import { CronCapability, handler, Runner, type Runtime } from "@chainlink/cre-sdk"
import { z } from "zod"

const configSchema = z.object({
  schedule: z.string(),
})

type Config = z.infer<typeof configSchema>

const onCronTrigger = (_runtime: Runtime<Config>) => {
  const result = myExtension().compute()
  return JSON.stringify({ result })
}

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>({ configSchema })
  await runner.run(initWorkflow)
}
```

### 3. Update your Makefile to pass `--plugin`

```makefile
JAVY_PLUGIN := $(abspath ./node_modules/@chainlink/cre-sdk-javy-plugin)
PLUGIN_PKG  := $(abspath ./node_modules/@acme/my-cre-plugin)

.PHONY: build clean

build:
	mkdir -p wasm
	CRE_SDK_JAVY_PLUGIN_HOME="$(JAVY_PLUGIN)" bun cre-compile \
		--plugin $(PLUGIN_PKG)/dist/plugin.wasm \
		./main.ts \
		./wasm/workflow.wasm

clean:
	rm -rf wasm
```

`--plugin` tells `cre-compile` to link the prebuilt plugin WASM into the final binary instead of the default CRE SDK plugin.

> **CAUTION: Version compatibility**
>
> A prebuilt `.plugin.wasm` must be compiled against a version of `@chainlink/cre-sdk-javy-plugin` that is **less than
> or equal to** the version included in your SDK. Check the plugin package's documentation for its `cre-sdk-javy-plugin`
> peer dependency version and verify it is compatible with your installed SDK version before using it.

## Mode 2: Source extensions (`--cre-exports`)

In source mode, you write a Rust crate and pass its directory to `cre-compile` with `--cre-exports`. The compiler builds your crate and links it alongside the SDK plugin at compile time. You can pass multiple `--cre-exports` flags to include several extensions in one binary.

This mode gives you full control over your Rust code and doesn't require distributing a prebuilt `.wasm`.

### 1. Create a Rust crate

Create a new directory for your extension with a `Cargo.toml` and `src/lib.rs`.

**`Cargo.toml`:**

```toml
# Use underscores in the crate name, not hyphens — the SDK generates a host
# crate that calls {name}::register(), so hyphens would produce invalid Rust.
[package]
name = "my_plugin"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["lib"]

[dependencies]
cre_wasm_exports = { path = "../node_modules/@chainlink/cre-sdk-javy-plugin/src/cre_wasm_exports" }
javy-plugin-api = "6.0.0"
```

**`src/lib.rs`:**

```rust
use cre_wasm_exports::extend_wasm_exports;
use javy_plugin_api::javy::quickjs::prelude::*;
use javy_plugin_api::javy::quickjs::{Ctx, Object};

pub fn register(ctx: &Ctx<'_>) {
    let obj = Object::new(ctx.clone()).unwrap();
    obj.set(
        "greet",
        Func::from(|| -> String { "Hello from Rust".to_string() }),
    )
    .unwrap();
    extend_wasm_exports(ctx, "myPlugin", obj);
}
```

`extend_wasm_exports` registers your object under the global name `"myPlugin"`. Whatever name you use here is what you reference in your TypeScript accessor.

### 2. Create a TypeScript accessor

```typescript
import { createExtensionAccessor } from "@chainlink/cre-sdk-javy-plugin/runtime/validate-extension"
import { z } from "zod"

const myPluginSchema = z.object({
  greet: z.function().args().returns(z.string()),
})

export type MyPlugin = z.infer<typeof myPluginSchema>

declare global {
  var myPlugin: MyPlugin
}

export const myPlugin = createExtensionAccessor("myPlugin", myPluginSchema)
```

### 3. Call it from your workflow

```typescript
import { myPlugin } from "./my-plugin-accessor"
import { CronCapability, handler, Runner, type Runtime } from "@chainlink/cre-sdk"
import { z } from "zod"

const configSchema = z.object({ schedule: z.string() })
type Config = z.infer<typeof configSchema>

const onCronTrigger = (_runtime: Runtime<Config>) => {
  return JSON.stringify({ result: myPlugin().greet() })
}

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>({ configSchema })
  await runner.run(initWorkflow)
}
```

### 4. Update your Makefile to pass `--cre-exports`

```makefile
JAVY_PLUGIN := $(abspath ./node_modules/@chainlink/cre-sdk-javy-plugin)

.PHONY: build clean

build:
	mkdir -p wasm
	CRE_SDK_JAVY_PLUGIN_HOME="$(JAVY_PLUGIN)" bun cre-compile \
		--cre-exports ./my-plugin \
		./index.ts \
		./wasm/workflow.wasm

clean:
	rm -rf wasm
```

To include multiple Rust extensions in one binary, chain additional `--cre-exports` flags:

```makefile
build:
	mkdir -p wasm
	CRE_SDK_JAVY_PLUGIN_HOME="$(JAVY_PLUGIN)" bun cre-compile \
		--cre-exports ./my-plugin-a \
		--cre-exports ./my-plugin-b \
		./index.ts \
		./wasm/workflow.wasm
```

## Try it yourself

Walk through source extension mode end to end. This uses Mode 2 (`--cre-exports`) since it works with a local Rust crate you write yourself — no externally published plugin package required.

**Prerequisites:** Rust with `wasm32-wasip1` (see [Prerequisites](#prerequisites) above). If you don't have a CRE project yet, create one with `cre init` and follow the [Getting Started guide](/cre/getting-started/overview). Once you're done, your working directory structure should look like this:

```
my-project/
├── project.yaml
├── secrets.yaml
└── my-workflow/
    ├── workflow.yaml
    ├── main.ts           ← your workflow entry point
    ├── main.test.ts
    ├── package.json
    ├── bun.lock
    ├── tsconfig.json
    └── node_modules/
```

**1. Convert your workflow to a custom build:**

Run this from your project root:

```bash
cre workflow custom-build ./my-workflow
```

This adds a `Makefile` to `my-workflow/` and updates `workflow.yaml` to point at the compiled binary. See [Custom WASM Builds](/cre/guides/operations/custom-build) for details.

**2. Create the Rust plugin crate:**

From your project root, create the crate directory and its source file:

```bash
mkdir -p my-workflow/my-plugin/src
```

Create `my-workflow/my-plugin/Cargo.toml`:

```toml
# my-workflow/my-plugin/Cargo.toml
# Note: use underscores in the crate name, not hyphens — the SDK uses this
# as a Rust identifier when generating the host crate.
[package]
name = "my_plugin"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["lib"]

[dependencies]
# cre_wasm_exports ships inside the installed cre-sdk-javy-plugin package
cre_wasm_exports = { path = "../node_modules/@chainlink/cre-sdk-javy-plugin/src/cre_wasm_exports" }
javy-plugin-api = "6.0.0"
```

Create `my-workflow/my-plugin/src/lib.rs`:

```rust
// my-workflow/my-plugin/src/lib.rs
use cre_wasm_exports::extend_wasm_exports;
use javy_plugin_api::javy::quickjs::prelude::*;
use javy_plugin_api::javy::quickjs::{Ctx, Object};

pub fn register(ctx: &Ctx<'_>) {
    let obj = Object::new(ctx.clone()).unwrap();
    obj.set(
        "greet",
        Func::from(|| -> String { "Hello from Rust".to_string() }),
    )
    .unwrap();
    // "myPlugin" is the global name your TypeScript accessor will reference
    extend_wasm_exports(ctx, "myPlugin", obj);
}
```

**3. Create a TypeScript accessor:**

Create `my-workflow/my-plugin-accessor.ts`. This file gives you a type-safe handle to the Rust global:

```typescript
// my-workflow/my-plugin-accessor.ts
import { createExtensionAccessor } from "@chainlink/cre-sdk-javy-plugin/runtime/validate-extension"
import { z } from "zod"

const myPluginSchema = z.object({
  greet: z.function().args().returns(z.string()),
})

declare global {
  var myPlugin: z.infer<typeof myPluginSchema>
}

// "myPlugin" must match the name passed to extend_wasm_exports in lib.rs
export const myPlugin = createExtensionAccessor("myPlugin", myPluginSchema)
```

**4. Call the Rust function from your workflow:**

Open `my-workflow/main.ts` and make the highlighted changes:

Code snippet for my-workflow/main.ts:

```typescript
import { CronCapability, handler, Runner, type Runtime } from "@chainlink/cre-sdk"
import { myPlugin } from "./my-plugin-accessor"

export type Config = {
  schedule: string
}

export const onCronTrigger = (runtime: Runtime<Config>): string => {
  runtime.log("Hello world! Workflow triggered.")
  const greeting = myPlugin().greet()
  runtime.log(`Rust says: ${greeting}`)
  return greeting
}

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

**5. Update the Makefile to pass `--cre-exports`:**

Open `my-workflow/Makefile` (created by `cre workflow custom-build`) and replace its contents with the following (highlighted line is the key addition):

Code snippet for my-workflow/Makefile:

```bash
JAVY_PLUGIN := $(abspath ./node_modules/@chainlink/cre-sdk-javy-plugin)

.PHONY: build clean

build:
	mkdir -p wasm
	CRE_SDK_JAVY_PLUGIN_HOME="$(JAVY_PLUGIN)" bun cre-compile \
		--cre-exports ./my-plugin \ # highlight-line
		./main.ts \
		./wasm/workflow.wasm

clean:
	rm -rf wasm
```

Your workflow directory should now look like this:

```
my-workflow/
├── workflow.yaml
├── main.ts
├── main.test.ts
├── package.json
├── bun.lock
├── tsconfig.json
├── Makefile                  ← updated
├── my-plugin-accessor.ts     ← new
├── my-plugin/                ← new Rust crate
│   ├── Cargo.toml
│   └── src/
│       └── lib.rs
├── node_modules/
└── wasm/                     ← workflow.wasm appears after simulate (or `make build`)
    └── workflow.wasm
```

**6. Simulate:**

From **my-project/** (the project root), run the simulator. It builds the workflow WASM first unless you point it at an existing binary, so you do not need to run `make build` manually before simulating:

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

You should see output like the following, confirming your Rust function was called from TypeScript:

```
[USER LOG] Hello world! Workflow triggered.
[USER LOG] Rust says: Hello from Rust

✓ Workflow Simulation Result:
"Hello from Rust"
```

> **NOTE: Simulation target**
>
> Make sure your CRE CLI is logged in (`cre whoami`) before simulating. See [Simulating
> Workflows](/cre/guides/operations/simulating-workflows) for details.

## Learn more

- [TypeScript Runtime Environment](/cre/concepts/typescript-wasm-runtime): Understand the Javy/QuickJS compilation pipeline and its constraints
- [Custom WASM Builds](/cre/guides/operations/custom-build): How to take ownership of the `cre-compile` step with a custom `Makefile`
- [Simulating Workflows](/cre/guides/operations/simulating-workflows): Test your workflow locally before deployment