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.wasmthat 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-compileis downloaded automatically when you runbun installvia apostinstallhook โ no manual setup required. -
Custom WASM builds enabled for your workflow โ Rust plugins are injected via
cre-compileflags, which you control from a customMakefile. If your workflow still uses automatic compilation, convert it first:cre workflow custom-build ./my-workflowSee 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:
| 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):
| 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
- TypeScript Runtime Environment: Understand the Javy/QuickJS compilation pipeline and its constraints
- Custom WASM Builds: How to take ownership of the
cre-compilestep with a customMakefile - Simulating Workflows: Test your workflow locally before deployment