Custom Rust Plugins

TypeScript workflows run inside a Javy-compiled WebAssembly 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 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 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 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

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:

    cre workflow custom-build ./my-workflow
    

    See Custom WASM Builds 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:

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:

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:

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

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.

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:

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

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

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

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

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:

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 above). If you don't have a CRE project yet, create one with cre init and follow the Getting Started guide. 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:

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 for details.

2. Create the Rust plugin crate:

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

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

Create my-workflow/my-plugin/Cargo.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:

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

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

my-workflow/main.ts
Typescript
1 import { CronCapability, handler, Runner, type Runtime } from "@chainlink/cre-sdk"
2 import { myPlugin } from "./my-plugin-accessor"
3
4 export type Config = {
5 schedule: string
6 }
7
8 export const onCronTrigger = (runtime: Runtime<Config>): string => {
9 runtime.log("Hello world! Workflow triggered.")
10 const greeting = myPlugin().greet()
11 runtime.log(`Rust says: ${greeting}`)
12 return greeting
13 }
14
15 export const initWorkflow = (config: Config) => {
16 const cron = new CronCapability()
17 return [handler(cron.trigger({ schedule: config.schedule }), onCronTrigger)]
18 }
19
20 export async function main() {
21 const runner = await Runner.newRunner<Config>()
22 await runner.run(initWorkflow)
23 }
24

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

my-workflow/Makefile
Shell
1 JAVY_PLUGIN := $(abspath ./node_modules/@chainlink/cre-sdk-javy-plugin)
2
3 .PHONY: build clean
4
5 build:
6 mkdir -p wasm
7 CRE_SDK_JAVY_PLUGIN_HOME="$(JAVY_PLUGIN)" bun cre-compile \
8 --cre-exports ./my-plugin \
9 ./main.ts \
10 ./wasm/workflow.wasm
11
12 clean:
13 rm -rf wasm
14

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:

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"

Learn more

Get the latest Chainlink content straight to your inbox.