# Building a Reusable Library
Source: https://docs.chain.link/cre/guides/workflow/building-a-library-ts
Last Updated: 2026-08-14

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

This guide shows you how to package reusable integration logic (for example, a Slack notifier, a PagerDuty client, or a price-feed parser) as a standard npm package that other developers can install and import directly into their own CRE TypeScript workflows.

This guide is not a tutorial on writing a workflow itself: if you're new to CRE, start with the [getting-started guide](/cre/getting-started/overview) first.

## Who this is for

Any developer who wants to publish a reusable package to npm that other people's CRE workflows will depend on.

## Your library runs where the workflow runs

A CRE TypeScript workflow is not run by Node.js. The workflow is transpiled and compiled into a single WebAssembly binary that executes inside [QuickJS](https://bellard.org/quickjs), a minimal JavaScript engine embedded via [Javy](https://github.com/bytecodealliance/javy). When a workflow author adds your package as a dependency, your code gets bundled into that same WASM binary, so it runs under the exact same restrictions as the workflow's own code. See [TypeScript Runtime Environment](/cre/concepts/typescript-wasm-runtime) for the full compilation pipeline.

Running inside that WASM sandbox has two direct consequences for your library:

- There is no Node.js runtime underneath your library at execution time: no `fs`, no `http`, no native modules.
- There is no network stack for your code to call into directly. All non-deterministic work (HTTP calls, secrets, blockchain reads/writes) must go through CRE SDK capability APIs, which the DON executes and brings to consensus on your library's behalf.

Design your library around these constraints from the start. Don't write a generic Node or browser package and assume it will run unmodified. Instead, build it directly against the CRE SDK's runtime primitives.

## Pitfalls

Because your library runs inside the same WASM sandbox as the workflow, the code you write can't rely on the Node.js or browser runtimes you're used to. The sections below cover the pitfalls that trip up most library authors: missing Node.js built-ins, no standard HTTP client, no top-level `async`/`await` around SDK calls, non-determinism, and numeric precision for onchain values. Read through them before you start, and keep them in mind as you design your library's API.

### No Node.js built-ins

QuickJS provides standard ECMAScript (`Map`, `Set`, `Promise`, most ES2020+ syntax) but not the Node.js API surface. **The following Node.js built-in modules are unavailable and will fail at compile or runtime** if your library, or one of its transitive dependencies, imports them:

| Node.js built-in | What it's typically used for   |
| ---------------- | ------------------------------ |
| `fs`             | File system access             |
| `path`           | Path manipulation              |
| `crypto`         | Hashing, signing, random bytes |
| `process`        | Process info, `process.env`    |
| `http`, `https`  | HTTP servers and clients       |
| `net`            | TCP sockets                    |
| `stream`         | Streaming data                 |
| `child_process`  | Spawning subprocesses          |
| `os`             | Operating system info          |
| `worker_threads` | Worker threads                 |
| `cluster`        | Multi-process clustering       |
| `dgram`          | UDP sockets                    |
| `dns`            | DNS resolution                 |
| `tls`            | TLS/SSL                        |
| `vm`             | Running code in a VM           |
| `zlib`           | Compression                    |
| `readline`       | Reading input line by line     |
| `events`         | Node's `EventEmitter`          |
| `util`           | Node's `promisify`, etc.       |

These restrictions have several practical implications for the code you write in your library:

- **`Buffer` is available and is the standard way to base64-encode request bodies.**

  Despite `buffer` being a Node built-in in principle, every CRE HTTP example in this guide uses `Buffer.from(bytes).toString("base64")` to encode request bodies, and it works the same in simulation and production. Use it the way the reference examples do instead of working around it with manual `btoa`/`String.fromCharCode` calls. `TextEncoder`/`TextDecoder` are also available and are the standard way to convert between strings and `Uint8Array`.

- **No `process.env`.**

  The `process` global doesn't exist in the WASM sandbox, so `process.env` is unavailable. Accept configuration through your library's function arguments, and read secrets with `runtime.getSecret()` (see [Secrets](/cre/guides/workflow/secrets)).

- **No Node `crypto`.**

  If you need hashing or signing, use a pure-JS library with no native bindings. [Noble](https://paulmillr.com/noble/) is a popular JavaScript cryptography library with minimal dependencies that works well with QuickJS. Always verify any third-party library in simulation before publishing.

- **No `setTimeout`/`setInterval`.**

  The WASM execution model is synchronous; there is no event loop to schedule against.

Before depending on any third-party package inside your library, check its `package.json` and source for the built-ins above. When in doubt, check the [QuickJS Node.js compatibility reference](https://sebastianwessel.github.io/quickjs/docs/module-resolution/node-compatibility.html), and confirm with `cre workflow simulate` in a throwaway test workflow, since simulation runs your code in the same WASM environment as production, so incompatibilities surface immediately.

### Alternatives to common Node built-ins

| Instead of...                            | Use...                                                                                     | Notes                                                                                                                                                       |
| ---------------------------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `node:crypto`                            | [`@noble/hashes`, `@noble/curves`](https://paulmillr.com/noble/)                           | Pure JS, no native bindings.                                                                                                                                |
| `ethers` (uses `node:crypto` internally) | [`viem`](https://viem.sh/)                                                                 | Verified compatible. Use for ABI encoding/decoding, address/unit utilities, and general Ethereum type handling.                                             |
| `axios`, `node-fetch`, `got`, `undici`   | `HTTPClient` from `@chainlink/cre-sdk`                                                     | **Not optional** since there is no working substitute; every HTTP call must go through the SDK's node-mode and consensus path.                              |
| `ws` (WebSockets)                        | Poll via `HTTPClient` on a cron or HTTP trigger instead                                    | Persistent socket connections are not supported in the WASM sandbox.                                                                                        |
| `dotenv` / `process.env`                 | `runtime.getSecret()` or function parameters                                               | There is no `process`, see above.                                                                                                                           |
| `import { Buffer } from "node:buffer"`   | The global `Buffer` (already available, no import needed)                                  | `Buffer` is available as a global, so importing it from `node:buffer` is unnecessary.                                                                       |
| `uuid` / `crypto.randomUUID()`           | `Math.random()`-based generation inside the CRE runtime, or a uuid package's pure-JS build | Some `uuid` package builds pull in `node:crypto` for `randomUUID`. Verify with `cre workflow simulate`.                                                     |
| `lodash`, `date-fns`, `dayjs`, `zod`     | Generally fine as-is                                                                       | Pure-JS utility/validation libraries with no Node built-ins typically work unmodified. `zod` is a documented SDK dependency. Still confirm with simulation. |
| Node's `events` (`EventEmitter`)         | Plain callbacks/arrays, or a pure-JS emitter with zero dependencies                        | Node's own `EventEmitter` isn't available; most third-party emitters that don't import `node:events` work fine.                                             |

> **NOTE: Verify before shipping**
>
> Only Noble, viem, and zod are dependencies used directly by the CRE SDK itself. The alternatives above are known to work because they are pure JS with no Node built-ins, but always confirm with `cre workflow simulate` before publishing your library.

### No standard HTTP client

Do not depend on `axios`, `node-fetch`, `ws`, or any HTTP or socket library, since they all sit on top of Node's `http`/`net`/`stream` modules and will not work. Additionally, there is no bare global `fetch` you can rely on directly from a library the way you would in a browser or in Node 18+.

Instead, all outbound requests must go through [`HTTPClient`](/cre/reference/sdk/http-client-ts) from `@chainlink/cre-sdk`. Beyond being a technical requirement, this is what gives the request DON-wide consensus. An HTTP call in CRE runs independently on every node in the DON (node mode), and the SDK aggregates the individual results into one trusted value before the workflow continues. A library that shells out to `axios` would bypass this consensus mechanism entirely, and even if it appeared to run, it would fail in production since the DON nodes would never agree on a single result.

Consequence for library design: your exported functions should take a CRE `Runtime` (or, in some cases, a `NodeRuntime`) as a parameter, and use it to construct requests via `HTTPClient`, exactly as a workflow author would inline. Keep your library a thin, well-tested wrapper around that `Runtime`-based request pattern.

```typescript
// src/index.ts, inside your library package
import { HTTPClient, consensusIdenticalAggregation, ok, type Runtime, type NodeRuntime } from "@chainlink/cre-sdk"

export interface PagerDutyOptions {
  routingKeySecret: string
}

export interface PagerDutyAlert {
  summary: string
  severity: "critical" | "error" | "warning" | "info"
  source: string
}

export class PagerDuty {
  constructor(private options: PagerDutyOptions) {}

  trigger(runtime: Runtime<unknown>, alert: PagerDutyAlert): { statusCode: number } {
    const sendAlert = (nodeRuntime: NodeRuntime<unknown>): { statusCode: number } => {
      const routingKey = nodeRuntime.getSecret({ id: this.options.routingKeySecret }).result().value

      const payload = {
        routing_key: routingKey,
        event_action: "trigger",
        payload: alert,
      }

      const bodyBytes = new TextEncoder().encode(JSON.stringify(payload))
      const body = Buffer.from(bodyBytes).toString("base64")

      const httpClient = new HTTPClient()
      const response = httpClient
        .sendRequest(nodeRuntime, {
          url: "https://events.pagerduty.com/v2/enqueue",
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body,
          // Prevents every node from firing a duplicate alert
          cacheSettings: { readFromCache: true, maxAgeMs: 60_000 },
        })
        .result()

      if (!ok(response)) {
        throw new Error(`PagerDuty request failed with status ${response.statusCode}`)
      }

      return { statusCode: response.statusCode }
    }

    return runtime.runInNodeMode(sendAlert, consensusIdenticalAggregation<{ statusCode: number }>())().result()
  }
}
```

Notes on this pattern:

- Secrets are only ever resolved with `runtime.getSecret()` / `nodeRuntime.getSecret()` (backed by the Vault DON), never read from environment variables or hardcoded. Your library should accept a secret ID/name, not a raw credential, and let the consuming workflow own the actual value in [CRE secrets](/cre/guides/workflow/secrets).
- `getSecret()` itself doesn't require node mode, but you'll typically call it from inside a `runInNodeMode` block anyway, since that's where you're already building the request that needs the secret value.
- Non-idempotent calls (POST/PUT/PATCH/DELETE) should set `cacheSettings` so a single DON-wide action isn't repeated once per node.
- Wrap HTTP calls in `runtime.runInNodeMode(...)` with an explicit [consensus aggregation](/cre/reference/sdk/consensus-ts) (`consensusIdenticalAggregation`, `consensusMedianAggregation`, or `ConsensusAggregationByFields` for multi-field objects) so the DON agrees on one result before your function returns control to the workflow.
- All SDK capability calls use the [`.result()` pattern](/cre/reference/sdk/core-ts#understanding-the-result-pattern) instead of `await`, see the next section.

### No top-level `async`/`await` around SDK calls

`Promise` and `async`/`await` exist in QuickJS, but CRE SDK capabilities (`HTTPClient`, `EVMClient`, secrets, and so on) do not use them, they use a synchronous `.result()` handshake between the WASM guest and the CRE host instead, because the guest/host boundary can't await across WASM calls. This means:

- Never use `Promise.race()` / `Promise.any()` around capability calls: result order between nodes is not deterministic and will break consensus.
- Write your library's functions as plain synchronous functions that call `.result()` inline, matching how workflow code itself is written. You can still use `async`/`await` for your own internal pure-JS logic that doesn't touch the SDK, but don't expose an `async` public API that wraps SDK calls, since it will mislead consumers into thinking they should `await` your function when they shouldn't (and can't, at the top level of a handler).

### Determinism

Every trigger callback in a CRE workflow runs independently on every DON node and must produce identical results (DON mode), except inside `runtime.runInNodeMode()` blocks, which are explicitly for non-deterministic, per-node work that then gets aggregated. If nodes diverge, they generate different request IDs for capability calls and consensus fails outright.

- **Never call `Date.now()` or `new Date()`.** Always use `runtime.now()` instead, it's available on both `Runtime` and `NodeRuntime`, so you don't need to branch on whether you're inside a `runInNodeMode` block.
- **`Math.random()` is safe, but only inside the CRE WASM runtime.** The CRE runtime overrides `Math.random()` with a seeded generator so every node produces the same sequence, and it isn't cryptographically secure, so don't use it for anything security-sensitive such as key generation or signing nonces. Randomness computed outside that runtime, such as a pre-computed value passed in or a dependency with its own PRNG, won't go through CRE's override and will diverge across nodes.
- **Never use `Promise.race()`, `Promise.any()`, or unpredictably-ordered `Promise.all()`** around `.result()` calls. Call `.result()` on each operation in a fixed order instead, for example always try API 1 then fall back to API 2. You can still initiate multiple requests before resolving any of them, as long as the resolution order never varies.
- **Prefer `Map`/`Set` over plain objects when output order matters, and avoid `for...in`.** Since ES2015, plain-object key enumeration order is defined by spec (integer-like keys ascending, then string keys in insertion order) and is consistent across DON nodes running the same engine, so it isn't a source of non-determinism on its own. The real risk is `for...in` also walking inherited enumerable properties. Use `Object.keys(obj)`, or `Map`/`Set`, which guarantee insertion order without that gotcha.

See [Avoiding Non-Determinism in Workflows](/cre/concepts/non-determinism-ts) for the complete guide, including LLM output handling.

### Numeric precision for onchain values

If your library produces or accepts values that will end up in a smart contract call, such as a price, an amount, or a `uint256`, use `bigint` (the `123n` suffix), never `number`. JavaScript `number` silently loses precision above `2^53`, which corrupts large integers without throwing an error. For scaling between human-readable decimals and fixed-point onchain representations, use viem's `parseUnits()`/`formatUnits()` (string-based, no floating-point math) rather than hand-rolled `* 10**18` arithmetic.

## Packaging your library

Structure it as an ordinary TypeScript npm package, there's no special CRE project layout for a library, only for a workflow.

### Project structure

A library is a minimal npm package with a single source file, its build config, and a README.

```
my-cre-library/
├── src/
│   └── index.ts
├── package.json
├── tsconfig.json
└── README.md
```

### `package.json`

The manifest declares the SDK as a peer dependency and points consumers at your compiled output.

```json
{
  "name": "my-cre-library",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "files": ["dist"],
  "scripts": {
    "build": "tsc"
  },
  "peerDependencies": {
    "@chainlink/cre-sdk": ">=1.0.0"
  },
  "devDependencies": {
    "@chainlink/cre-sdk": "latest",
    "typescript": "^5.9.0"
  }
}
```

Key points:

- Declare `@chainlink/cre-sdk` as a `peerDependency`, not a regular dependency. The consuming workflow already depends on `@chainlink/cre-sdk` directly, so a peer dependency avoids bundling two separate copies of the SDK into the same WASM binary and avoids version-mismatch surprises. Add it to `devDependencies` too so your own build and typecheck work.
- Ship plain compiled JS and `.d.ts` declarations. `tsc` output is enough, no bundler is required, since the workflow's own build step is what ultimately compiles everything down to WASM via Bun and Javy. Keep your compiled output free of Node-specific syntax; target `ES2020`/`ESNext` module output, not CommonJS.
- Don't ship a `postinstall` script, that's a workflow-project concern (`bunx cre-setup`), not a library concern.
- Keep your own dependency tree small. Everything you import gets bundled into the consuming workflow's single WASM binary, which is subject to production size and memory quotas (inspect them with `cre workflow limits export`, see [Testing Production Limits](/cre/guides/operations/understanding-limits)). A heavy library can push an otherwise-fine workflow over its quota.

### `tsconfig.json`

The compiler settings mirror the workflow's own so your output stays compatible with the WASM build.

```json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*.ts"]
}
```

## Publishing

Publish it like any other npm package:

```bash
npm run build
npm publish --access public
```

Nothing about `npm publish` itself is CRE-specific. The workflow author's `bun install` / `cre-setup` pipeline is what turns your published JS into something that runs inside the WASM sandbox.

## Using your library from a CRE workflow

The workflow author adds it like any other dependency:

```bash
cd my-workflow
bun add my-cre-library
```

Then imports and calls it from their workflow handler, passing in the `runtime` they already have:

```typescript
import { CronCapability, handler, Runner, type Runtime } from "@chainlink/cre-sdk"
import { PagerDuty } from "my-cre-library"

type Config = { schedule: string }

const pagerDuty = new PagerDuty({ routingKeySecret: "pager-duty-routing-key" })

const onCronTrigger = (runtime: Runtime<Config>): string => {
  pagerDuty.trigger(runtime, {
    summary: "ETH/USD price breached threshold",
    severity: "critical",
    source: "cre-workflow",
  })
  return "alert sent"
}

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

The workflow author still owns:

- Storing the actual secret value (`pager-duty-routing-key`) in their [CRE secrets](/cre/guides/workflow/secrets); your library only ever refers to it by name.
- Running `cre workflow simulate` to verify your library behaves correctly compiled into their WASM binary before deploying.

## Pre-publish checklist

Before you publish, run through this checklist to confirm your library follows every constraint covered in this guide. Each item maps to a pitfall or packaging requirement above.

- No import of `fs`, `path`, `crypto`, `process`, `http`, `https`, `net`, `stream`, or any other Node built-in (aside from `Buffer`, which is available), directly or transitively. See [No Node.js built-ins](#no-nodejs-built-ins).
- All outbound network calls go through `HTTPClient` (or `EVMClient` for chain reads/writes), never a third-party HTTP or socket library. See [No standard HTTP client](#no-standard-http-client).
- HTTP calls, time, and randomness are wrapped in `runtime.runInNodeMode()` with an explicit consensus aggregation, or delegated back to the caller's `runtime`. Secrets are resolved with `getSecret()`, typically from inside that same block. See [No standard HTTP client](#no-standard-http-client).
- No `Promise.race()`, `Promise.any()`, or order-unpredictable `Promise.all()` around `.result()` calls; resolution order is always fixed. See [No top-level `async`/`await` around SDK calls](#no-top-level-asyncawait-around-sdk-calls) and [Determinism](#determinism).
- No plain-object `for...in` iteration relying on inherited properties; use `Object.keys()`, or `Map`/`Set`, instead. See [Determinism](#determinism).
- No `async` public API wrapping SDK capability calls; expose the synchronous `.result()`-based pattern. See [No top-level `async`/`await` around SDK calls](#no-top-level-asyncawait-around-sdk-calls).
- Secrets are accepted as an ID/name, never as a literal value baked into your library. See [No standard HTTP client](#no-standard-http-client).
- `@chainlink/cre-sdk` is a `peerDependency`, not a bundled dependency. See [`package.json`](#packagejson).
- Verified end-to-end with `cre workflow simulate` in a real (throwaway) CRE workflow that depends on your published or local package. See [No Node.js built-ins](#no-nodejs-built-ins).

## Learn more

- [TypeScript Runtime Environment](/cre/concepts/typescript-wasm-runtime): QuickJS compatibility and the WASM compilation pipeline
- [Avoiding Non-Determinism in Workflows](/cre/concepts/non-determinism-ts): The complete determinism guide
- [SDK Reference: HTTP Client](/cre/reference/sdk/http-client-ts): Full `HTTPClient` API
- [SDK Reference: Core](/cre/reference/sdk/core-ts): `Runtime`, `NodeRuntime`, and the `.result()` pattern
- [Secrets](/cre/guides/workflow/secrets): Storing and retrieving secrets for a deployed workflow