Custom Rust Plugins

TypeScript workflows run inside a Javy-compiled WebAssembly binary. By default, that binary includes only the CRE SDK plugin layer, which includes the Rust code that bridges your TypeScript to CRE node capabilities. Custom Rust plugins let you inject your own Rust crates into that same layer, exposing new functionality as typed globals your TypeScript workflow can call directly.

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:

  • Performance-critical computation: Rust running natively in WASM is dramatically faster than interpreted JavaScript for anything compute-heavy, like cryptographic operations, numeric processing, data encoding
  • Native crate access: Pull in any Rust library from crates.io that targets wasm32-wasip1. This unlocks algorithms and formats the SDK doesn't expose (custom signature schemes, binary codecs, etc.)
  • Custom protocol verification: Verify offchain reports or proofs that require native crypto not available in QuickJS

Prerequisites

Required versions: TS SDK v1.6.0+

Before using custom Rust plugins, you need:

  • Rust stable toolchain with the wasm32-wasip1 target:

    rustup target add wasm32-wasip1
    
  • 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.

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:

rustup target add wasm32-wasip1

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/                     ← created by make build
    └── workflow.wasm

6. Build and simulate:

Run make build from inside your workflow directory, then simulate from the project root:

# From my-workflow/
make build

# From my-project/ (project root)
cd ..
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.