From 49c0d7e73fb9d17b5ff63e4dfe9a57dcc91a6492 Mon Sep 17 00:00:00 2001 From: Emmanuel Jacquier Date: Fri, 14 Aug 2026 08:42:54 -0400 Subject: [PATCH 1/6] added guide to create library/plugins for CRE --- src/config/sidebar.ts | 8 + .../operations/building-a-library-go.mdx | 286 ++++++++++++++++ .../operations/building-a-library-ts.mdx | 305 ++++++++++++++++++ src/content/cre/llms-full-go.txt | 279 ++++++++++++++++ src/content/cre/llms-full-ts.txt | 298 +++++++++++++++++ 5 files changed, 1176 insertions(+) create mode 100644 src/content/cre/guides/operations/building-a-library-go.mdx create mode 100644 src/content/cre/guides/operations/building-a-library-ts.mdx diff --git a/src/config/sidebar.ts b/src/config/sidebar.ts index 7000d697f3f..5089f583b70 100644 --- a/src/config/sidebar.ts +++ b/src/config/sidebar.ts @@ -566,6 +566,14 @@ export const SIDEBAR: Partial> = { title: "Custom Rust Plugins", url: "cre/guides/operations/custom-rust-plugins-ts", }, + { + title: "Building a Reusable Library", + url: "cre/guides/operations/building-a-library", + highlightAsCurrent: [ + "cre/guides/operations/building-a-library-ts", + "cre/guides/operations/building-a-library-go", + ], + }, ], }, { diff --git a/src/content/cre/guides/operations/building-a-library-go.mdx b/src/content/cre/guides/operations/building-a-library-go.mdx new file mode 100644 index 00000000000..26957ffed34 --- /dev/null +++ b/src/content/cre/guides/operations/building-a-library-go.mdx @@ -0,0 +1,286 @@ +--- +section: cre +date: Last Modified +title: "Building a Reusable Library" +sdkLang: "go" +pageId: "guides-operations-building-a-library" +metadata: + description: "How to build and publish a Go module that CRE workflows can import, including WASI/WASM restrictions and CRE SDK HTTP requirements." + datePublished: "2026-08-14" + lastModified: "2026-08-14" +--- + +import { Aside } from "@components" + +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 Go module that other developers can `go get` and import directly into their own CRE workflows. + +This is not a guide to writing a workflow itself — it assumes you're already familiar with that. 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 Go module that other people's CRE workflows will depend on. + +## The core constraint: your library runs where the workflow runs + +A CRE Go workflow isn't run as a normal Go binary. It's cross-compiled with `GOOS=wasip1 GOARCH=wasm` into a WebAssembly module that runs inside the CRE host's WASI sandbox. When a workflow author adds your module as a dependency, your code is compiled into that same binary — it runs under the exact same restrictions as the workflow's own code. + +Unlike the TypeScript SDK, which runs inside a stripped-down QuickJS engine, Go workflows compile with the **full Go standard library** available at compile time. This means far more of the ecosystem "just works" without compatibility checking — but the WASI sandbox still enforces real limits at runtime: + +- No filesystem access. There's no real disk backing the sandbox, so file I/O fails or is meaningless even though the code compiles. +- No arbitrary outbound network access. There's no raw socket layer to dial into — everything non-deterministic (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. +- Single-threaded, deterministic execution. All DON nodes must execute your code identically and produce the same output. + +Design your library around this from the start. Don't write it as a generic Go module that happens to shell out to the filesystem or network and hope it works under WASI — write it against the CRE SDK's runtime primitives. + +## Pitfall 1: No filesystem or arbitrary network access + +Your library (or one of its transitive dependencies) must not depend on: + +- **Filesystem access** — `os.Open`, `os.ReadFile`, `os.WriteFile`, config-file loaders, embedded file caches written to disk, and so on. There is no writable (or meaningfully readable) filesystem in the WASI sandbox the workflow runs in. +- **Direct network access** — `net.Dial`, `net/http`'s `http.Client`/`http.Get`, gRPC dialing, database drivers, or any other package that opens its own socket. These either fail at runtime or, worse, silently do nothing useful, because there's no outbound network stack available to your code directly — only to the CRE host, through capability calls. + +Anything your library needs from disk (default config, lookup tables, certificates) should be compiled in as Go constants, embedded with `//go:embed` (a compile-time read, not a runtime filesystem access — this is safe), or passed in as arguments by the caller. Anything it needs from the network must go through `http.Client`/`http.SendRequest` from `@chainlink/cre-sdk`'s Go equivalent, [`cre-sdk-go`](#pitfall-2-no-standard-http-client--use-the-cre-sdks-http-capability) — never a raw dependency that dials sockets itself. + +{/* prettier-ignore */} + + +## Pitfall 2: No standard HTTP client — use the CRE SDK's HTTP capability + +Do not depend on `net/http`, a REST client wrapper built on it, or any package that dials its own connections. Instead, all outbound requests must go through [`http.Client`](/cre/reference/sdk/http-client-go) from `github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http`. This isn't just a technical requirement — it's 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 dials out with `net/http` would bypass this consensus mechanism entirely, even if it somehow ran. + +Consequence for library design: your exported functions should take a `cre.Runtime` as a parameter, and use it (via `http.SendRequest` or `cre.RunInNodeMode`) to construct requests, exactly as a workflow author would inline. Your library is a thin, well-tested wrapper around this pattern — not a replacement for it. + +```go +// pagerduty.go — inside your library module +package pagerduty + +import ( + "encoding/json" + "fmt" + + "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http" + "github.com/smartcontractkit/cre-sdk-go/cre" +) + +type Options struct { + RoutingKeySecret string +} + +type Alert struct { + Summary string `json:"summary"` + Severity string `json:"severity"` + Source string `json:"source"` +} + +type Client struct { + options Options +} + +func New(options Options) *Client { + return &Client{options: options} +} + +func (c *Client) Trigger(runtime cre.Runtime, alert Alert) (int, error) { + sendAlert := func(config Options, nodeRuntime cre.NodeRuntime) (int, error) { + secret, err := nodeRuntime.GetSecret(&cre.SecretRequest{Id: config.RoutingKeySecret}).Await() + if err != nil { + return 0, fmt.Errorf("failed to get routing key: %w", err) + } + + payload := map[string]any{ + "routing_key": secret.Value, + "event_action": "trigger", + "payload": alert, + } + + body, err := json.Marshal(payload) + if err != nil { + return 0, fmt.Errorf("failed to marshal payload: %w", err) + } + + client := &http.Client{} + resp, err := client.SendRequest(nodeRuntime, &http.Request{ + Url: "https://events.pagerduty.com/v2/enqueue", + Method: "POST", + MultiHeaders: map[string]*http.HeaderValues{ + "Content-Type": {Values: []string{"application/json"}}, + }, + Body: body, + // Prevents every node from firing a duplicate alert + CacheSettings: &http.CacheSettings{Store: true}, + }).Await() + if err != nil { + return 0, fmt.Errorf("PagerDuty request failed: %w", err) + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return 0, fmt.Errorf("PagerDuty request failed with status %d", resp.StatusCode) + } + + return int(resp.StatusCode), nil + } + + promise := cre.RunInNodeMode(c.options, runtime, sendAlert, cre.ConsensusIdenticalAggregation[int]()) + return promise.Await() +} +``` + +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). +- Non-idempotent calls (POST/PUT/PATCH/DELETE) should set `CacheSettings` so a single DON-wide action isn't repeated once per node. +- Wrap non-deterministic work (HTTP, secrets) in `cre.RunInNodeMode(...)` with an explicit [consensus aggregation](/cre/reference/sdk/consensus-go) (`cre.ConsensusIdenticalAggregation`, `cre.ConsensusMedianAggregation`, or `cre.ConsensusAggregationFromTags` for a tagged result struct) so the DON agrees on one result before your function returns control to the workflow. +- `Body` on `http.Request` is a plain `[]byte` — unlike the TypeScript SDK, there's no base64-encoding step to worry about. +- All SDK capability calls return a [`Promise[T]`](/cre/reference/sdk/core-go#promise) that you resolve with `.Await()` — see the next section. + +## Pitfall 3: `Promise`/`Await` concurrency, not goroutines + +The Go SDK doesn't use `async`/`await` keywords — it uses a `Promise[T]` type with `.Await()`, `cre.Then()`, and `cre.ThenPromise()` for chaining (see [Core SDK Reference](/cre/reference/sdk/core-go#promise)). This exists because the underlying operation only actually runs when you call `.Await()` — building a promise chain without awaiting it does nothing. + +The pitfall specific to Go: don't reach for goroutines, channels, or `sync` primitives (`WaitGroup`, `Mutex`, and so on) to run multiple SDK calls "concurrently" inside your library. CRE workflows execute in a single-threaded WASM environment — using Go's concurrency primitives to fan out capability calls doesn't get you real parallelism, and a `select` across multiple channels picks a ready channel non-deterministically, which will cause consensus failures if the result depends on which one "won." + +- Never use `select` with multiple ready channels to decide which result to use or which branch to take — see [Avoiding Non-Determinism in Workflows](/cre/concepts/non-determinism-go#3-concurrency-and-channel-selection). +- If you need to make several capability calls, initiate them in a fixed order and resolve them (`.Await()`) in that same fixed order. You can build up several `Promise` values before awaiting any of them, but the order you await them in must never vary. +- `cre.Then()` / `cre.ThenPromise()` are the idiomatic way to chain dependent async steps without nested `.Await()` calls — prefer them over manually orchestrating goroutines. + +## Pitfall 4: determinism + +Every trigger callback in a CRE workflow runs independently on every DON node and must produce identical results (DON mode), except inside `cre.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. If your library does any of its own computation — not just HTTP or secrets — keep it deterministic: + +- **Never iterate a Go map directly** when the order affects your output. Go maps intentionally randomize iteration order. Use [`cre.OrderedEntries`](/cre/reference/sdk/core-go#creorderedentries-and-creorderedentriesfunc) (or `cre.OrderedEntriesFunc` for non-`cmp.Ordered` keys) instead of `for k, v := range someMap`. +- **Use `encoding/json` v1, not v2.** The v2 library uses randomized hashing for field ordering, which serializes the same struct differently across nodes. +- **Use `proto.MarshalOptions{Deterministic: true}.Marshal()`** if you serialize Protocol Buffers — the default `proto.Marshal` doesn't guarantee field order. +- **Never call `time.Now()` or any other `time` package clock function.** Accept the current time as a parameter (the workflow author gets it from `runtime.Now()`), or compute it inside a `RunInNodeMode` block. +- **Never use `math/rand` (or `crypto/rand`) directly.** Use `runtime.Rand()` from the CRE SDK, which provides a consensus-safe generator so every node produces the same sequence. +- **Never use `select` with multiple ready channels** to make a decision — see Pitfall 3 above. + +{/* prettier-ignore */} + + +## Pitfall 5: numeric precision for onchain and price values + +If your library produces or accepts values that will end up in a smart contract call — a price, an amount, a `uint256` — use `*big.Int`, never a plain `int`/`int64`/`float64`. For monetary or price values that aren't already onchain integers, use a fixed-point decimal type such as [`github.com/shopspring/decimal`](https://github.com/shopspring/decimal) rather than `float64`, which loses precision under repeated arithmetic. This matches the convention used throughout the CRE Go SDK's own examples. + +## Packaging your library + +Structure it as an ordinary Go module — there's no special CRE project layout for a library, only for a workflow (which is itself a package inside the consuming project's single Go module). + +``` +my-cre-library/ +├── go.mod +├── go.sum +├── pagerduty.go +└── README.md +``` + +**`go.mod`:** + +``` +module github.com/you/my-cre-library + +go 1.25.3 + +require github.com/smartcontractkit/cre-sdk-go v1.6.0 +``` + +Key points: + +- Use a normal `require` for `github.com/smartcontractkit/cre-sdk-go` — there's no `peerDependency` concept in Go modules, and you don't need one. Go's module resolution (minimal version selection) automatically unifies the SDK version across your library and the consuming workflow's `go.mod` to a single compatible version, so there's no risk of bundling two copies the way there is with npm. +- **Don't add a `//go:build wasip1` build tag to your library's own files** unless you have a specific reason to (for example, wrapping a WASI-only import). `cre.Runtime`, `cre.NodeRuntime`, and the capability client types are plain, portable Go — they compile under any `GOOS`. Only the workflow's own entry point (the file calling `wasm.NewRunner`) needs the `wasip1` tag. Keeping your library tag-free means your library's own `go test` suite — and any consumer's tests that exercise your library — run under the host's native `GOOS` instead of requiring a full WASM build. +- Take advantage of the SDK's mock packages where they exist (for example, `github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm/mock` for EVM calls) to unit test logic that depends on `cre.Runtime` without running `cre workflow simulate`. There is currently no equivalent mock package for the HTTP capability, so HTTP-calling code still needs an end-to-end `cre workflow simulate` pass to verify. +- Keep your own dependency tree small. Everything you import gets compiled 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 — or one with a dependency that unexpectedly requires cgo — can push an otherwise-fine workflow over its quota or break its WASM build entirely (cgo is not supported under `wasip1`). + +## Publishing + +Go modules don't have a central publish step or registry like npm — you publish by pushing your code to a Git repository (typically GitHub) and tagging a semver release: + +```bash +git tag v1.0.0 +git push origin v1.0.0 +``` + +Your module path (in `go.mod`) should match the repository's import path, for example `github.com/you/my-cre-library`. Nothing about tagging a release is CRE-specific — the workflow author's own `go build`/`cre workflow simulate` pipeline is what turns your published Go code into something that runs inside the WASM sandbox, on their side. + +## Using your library from a CRE workflow + +Recall that a Go CRE project is a single Go module — the workflow author adds your library with `go get` from the project root (not from inside the workflow subdirectory): + +```bash +go get github.com/you/my-cre-library@v1.0.0 +``` + +Then imports and calls it from their workflow handler, passing in the `runtime` they already have: + +```go +//go:build wasip1 + +package main + +import ( + "log/slog" + + "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron" + "github.com/smartcontractkit/cre-sdk-go/cre" + "github.com/smartcontractkit/cre-sdk-go/cre/wasm" + "github.com/you/my-cre-library" +) + +type Config struct { + Schedule string `json:"schedule"` +} + +var pagerDutyClient = pagerduty.New(pagerduty.Options{RoutingKeySecret: "pager-duty-routing-key"}) + +func onCronTrigger(config *Config, runtime cre.Runtime, trigger *cron.Payload) (string, error) { + _, err := pagerDutyClient.Trigger(runtime, pagerduty.Alert{ + Summary: "ETH/USD price breached threshold", + Severity: "critical", + Source: "cre-workflow", + }) + if err != nil { + return "", err + } + return "alert sent", nil +} + +func InitWorkflow(config *Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[*Config], error) { + return cre.Workflow[*Config]{ + cre.Handler(cron.Trigger(&cron.Config{Schedule: config.Schedule}), onCronTrigger), + }, nil +} + +func main() { + wasm.NewRunner(cre.ParseJSON[*Config]).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 + +- [ ] No filesystem access (`os.Open`, `os.ReadFile`, `os.WriteFile`, and similar) anywhere in your library or its dependencies. +- [ ] No direct network access (`net.Dial`, `net/http`, database drivers, gRPC dialing) — all outbound calls go through `http.Client`/`http.SendRequest` (or `evm.Client` for chain reads/writes). +- [ ] All non-deterministic work (HTTP, secrets, time, randomness) is wrapped in `cre.RunInNodeMode()` with an explicit consensus aggregation, or delegated back to the caller's `runtime`. +- [ ] No goroutines, channels, or `select` across multiple channels used to make a decision that affects your function's output. +- [ ] No direct map iteration where order affects the output — use `cre.OrderedEntries`/`cre.OrderedEntriesFunc`. +- [ ] `encoding/json` v1 (not v2) and, if applicable, `proto.MarshalOptions{Deterministic: true}` for serialization. +- [ ] No `time.Now()`, `math/rand`, or `crypto/rand` — use `runtime.Now()` and `runtime.Rand()`. +- [ ] `*big.Int` for onchain integer values; a decimal type (not `float64`) for prices and other precision-sensitive values. +- [ ] Secrets are accepted as an ID/name, never as a literal value baked into your library. +- [ ] No `//go:build wasip1` tag on library files unless truly required — keep your library testable with plain `go test`. +- [ ] Verified end-to-end with `cre workflow simulate` in a real (throwaway) CRE workflow that depends on your published or local module. + +## Learn more + +- [Avoiding Non-Determinism in Workflows](/cre/concepts/non-determinism-go): The complete determinism guide +- [SDK Reference: HTTP Client](/cre/reference/sdk/http-client-go): Full `http.Client` API +- [SDK Reference: Core](/cre/reference/sdk/core-go): `Runtime`, `NodeRuntime`, `Promise`, and `cre.OrderedEntries` +- [SDK Reference: Consensus & Aggregation](/cre/reference/sdk/consensus-go): Aggregation functions for `cre.RunInNodeMode` +- [Secrets](/cre/guides/workflow/secrets): Storing and retrieving secrets for a deployed workflow diff --git a/src/content/cre/guides/operations/building-a-library-ts.mdx b/src/content/cre/guides/operations/building-a-library-ts.mdx new file mode 100644 index 00000000000..478cbb97956 --- /dev/null +++ b/src/content/cre/guides/operations/building-a-library-ts.mdx @@ -0,0 +1,305 @@ +--- +section: cre +date: Last Modified +title: "Building a Reusable Library" +sdkLang: "ts" +pageId: "guides-operations-building-a-library" +metadata: + description: "How to build and publish an npm package that CRE TypeScript workflows can import, including QuickJS/WASM restrictions and CRE SDK HTTP requirements." + datePublished: "2026-08-14" + lastModified: "2026-08-14" +--- + +import { Aside } from "@components" + +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 is not a guide to writing a workflow itself — it assumes you're already familiar with that. 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. + +## The core constraint: your library runs where the workflow runs + +A CRE TypeScript workflow isn't run by Node.js. It's 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 — 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. + +That means: + +- 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 this from the start. Don't write it as a generic Node or browser package and hope it happens to work — write it against the CRE SDK's runtime primitives. + +## Pitfall 1: No Node.js built-ins + +QuickJS provides standard ECMAScript (`Map`, `Set`, `Promise`, most ES2020+ syntax) but not the Node.js API surface. The following are unavailable and will fail at compile or runtime if your library — or one of its transitive dependencies — imports them: + +``` +fs, path, crypto, process, http, https, net, stream, child_process, +os, worker_threads, cluster, dgram, dns, tls, vm, zlib, readline, +events (Node's EventEmitter), util (Node's promisify, etc.) +``` + +Practical implications: + +- **`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 documentation 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 — don't work 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`.** Never read configuration or credentials this way — there is no process. Accept configuration through your library's function arguments, and read secrets with `runtime.getSecret()` (see below). +- **No Node `crypto`.** If you need hashing or signing, use a pure-JS library with no native bindings. +- **No `setTimeout`/`setInterval`.** The WASM execution model is synchronous; there is no event loop to schedule against. + +{/* prettier-ignore */} + + +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 — 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 — see [Pitfall 2](#pitfall-2-no-standard-http-client--use-the-cre-sdks-http-capability). 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 aren't 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) | See the note above. | +| `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. | + +{/* prettier-ignore */} + + +## Pitfall 2: No standard HTTP client — use the CRE SDK's HTTP capability + +Do not depend on `axios`, `node-fetch`, `ws`, or any HTTP or socket library — they all sit on top of Node's `http`/`net`/`stream` modules and will not work. There is also 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`. This isn't just a technical requirement — it's 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, even if it somehow ran. + +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. Your library is a thin, well-tested wrapper around this pattern — not a replacement for it. + +```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, alert: PagerDutyAlert): { statusCode: number } { + const sendAlert = (nodeRuntime: NodeRuntime): { 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). +- Non-idempotent calls (POST/PUT/PATCH/DELETE) should set `cacheSettings` so a single DON-wide action isn't repeated once per node. +- Wrap non-deterministic work (HTTP, secrets) 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. + +## Pitfall 3: 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) don't 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 — 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). + +## Pitfall 4: 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. If your library does any of its own computation — not just HTTP or secrets — keep it deterministic: + +- **Never call `Date.now()` or `new Date()`** inside DON-mode code — accept the current time as a parameter (the workflow author gets it from `runtime.now()`), or compute it 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. What's unsafe is randomness computed outside that runtime — for example a pre-computed value passed in, or a dependency that ships its own PRNG or native random source — since that won't go through CRE's override and will diverge across nodes. This randomness is also **not cryptographically secure** — don't use it in your library for anything security-sensitive, such as key generation or signing nonces. +- **Never use `Promise.race()`, `Promise.any()`, or `Promise.all()` with unpredictable or non-fixed ordering** around `.result()` calls. `Promise.race()`/`Promise.any()` let different nodes "win" with different sources; unpredictable `Promise.all()` usage has the same failure mode if the set or order of promises isn't fixed ahead of time. Instead, call `.result()` on each operation in a fixed, hardcoded order — 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 order you resolve them in never varies. +- **Never iterate plain `Object` keys with `for...in`** when order affects the result — object key order isn't guaranteed by spec, even though engines usually preserve insertion order. Use `Object.keys(obj).sort()` for guaranteed-deterministic order. `Map` and `Set` are safe to iterate directly since they guarantee insertion order by specification — prefer them over plain objects when your library's output depends on element order. + +{/* prettier-ignore */} + + +## Pitfall 5: numeric precision for onchain values + +If your library produces or accepts values that will end up in a smart contract call — a price, an amount, 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. + +``` +my-cre-library/ +├── src/ +│ └── index.ts +├── package.json +├── tsconfig.json +└── README.md +``` + +**`package.json`:** + +```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 — it needs it to run at all — 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`** (mirrors the workflow's own compiler settings so output stays compatible): + +```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, on their side. + +## 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): 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() + 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 + +- [ ] 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. +- [ ] All outbound network calls go through `HTTPClient` (or `EVMClient` for chain reads/writes), never a third-party HTTP or socket library. +- [ ] All non-deterministic work (HTTP, secrets, time, randomness) is wrapped in `runtime.runInNodeMode()` with an explicit consensus aggregation, or delegated back to the caller's `runtime`. +- [ ] No `Promise.race()`, `Promise.any()`, or order-unpredictable `Promise.all()` around `.result()` calls — resolution order is always fixed. +- [ ] No plain-object `for...in` iteration where key order affects the output — sorted `Object.keys()`, or `Map`/`Set`, instead. +- [ ] No `async` public API wrapping SDK capability calls — expose the synchronous `.result()`-based pattern. +- [ ] Secrets are accepted as an ID/name, never as a literal value baked into your library. +- [ ] `@chainlink/cre-sdk` is a `peerDependency`, not a bundled dependency. +- [ ] Verified end-to-end with `cre workflow simulate` in a real (throwaway) CRE workflow that depends on your published or local package. + +## 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 diff --git a/src/content/cre/llms-full-go.txt b/src/content/cre/llms-full-go.txt index 1100a05c75c..d639231b245 100644 --- a/src/content/cre/llms-full-go.txt +++ b/src/content/cre/llms-full-go.txt @@ -12389,6 +12389,285 @@ You've now mastered the complete CRE development workflow! --- +# Building a Reusable Library +Source: https://docs.chain.link/cre/guides/operations/building-a-library-go +Last Updated: 2026-08-14 + +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 Go module that other developers can `go get` and import directly into their own CRE workflows. + +This is not a guide to writing a workflow itself — it assumes you're already familiar with that. 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 Go module that other people's CRE workflows will depend on. + +## The core constraint: your library runs where the workflow runs + +A CRE Go workflow isn't run as a normal Go binary. It's cross-compiled with `GOOS=wasip1 GOARCH=wasm` into a WebAssembly module that runs inside the CRE host's WASI sandbox. When a workflow author adds your module as a dependency, your code is compiled into that same binary — it runs under the exact same restrictions as the workflow's own code. + +Unlike the TypeScript SDK, which runs inside a stripped-down QuickJS engine, Go workflows compile with the **full Go standard library** available at compile time. This means far more of the ecosystem "just works" without compatibility checking — but the WASI sandbox still enforces real limits at runtime: + +- No filesystem access. There's no real disk backing the sandbox, so file I/O fails or is meaningless even though the code compiles. +- No arbitrary outbound network access. There's no raw socket layer to dial into — everything non-deterministic (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. +- Single-threaded, deterministic execution. All DON nodes must execute your code identically and produce the same output. + +Design your library around this from the start. Don't write it as a generic Go module that happens to shell out to the filesystem or network and hope it works under WASI — write it against the CRE SDK's runtime primitives. + +## Pitfall 1: No filesystem or arbitrary network access + +Your library (or one of its transitive dependencies) must not depend on: + +- **Filesystem access** — `os.Open`, `os.ReadFile`, `os.WriteFile`, config-file loaders, embedded file caches written to disk, and so on. There is no writable (or meaningfully readable) filesystem in the WASI sandbox the workflow runs in. +- **Direct network access** — `net.Dial`, `net/http`'s `http.Client`/`http.Get`, gRPC dialing, database drivers, or any other package that opens its own socket. These either fail at runtime or, worse, silently do nothing useful, because there's no outbound network stack available to your code directly — only to the CRE host, through capability calls. + +Anything your library needs from disk (default config, lookup tables, certificates) should be compiled in as Go constants, embedded with `//go:embed` (a compile-time read, not a runtime filesystem access — this is safe), or passed in as arguments by the caller. Anything it needs from the network must go through `http.Client`/`http.SendRequest` from `@chainlink/cre-sdk`'s Go equivalent, [`cre-sdk-go`](#pitfall-2-no-standard-http-client--use-the-cre-sdks-http-capability) — never a raw dependency that dials sockets itself. + + + + +## Pitfall 2: No standard HTTP client — use the CRE SDK's HTTP capability + +Do not depend on `net/http`, a REST client wrapper built on it, or any package that dials its own connections. Instead, all outbound requests must go through [`http.Client`](/cre/reference/sdk/http-client-go) from `github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http`. This isn't just a technical requirement — it's 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 dials out with `net/http` would bypass this consensus mechanism entirely, even if it somehow ran. + +Consequence for library design: your exported functions should take a `cre.Runtime` as a parameter, and use it (via `http.SendRequest` or `cre.RunInNodeMode`) to construct requests, exactly as a workflow author would inline. Your library is a thin, well-tested wrapper around this pattern — not a replacement for it. + +```go +// pagerduty.go — inside your library module +package pagerduty + +import ( + "encoding/json" + "fmt" + + "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http" + "github.com/smartcontractkit/cre-sdk-go/cre" +) + +type Options struct { + RoutingKeySecret string +} + +type Alert struct { + Summary string `json:"summary"` + Severity string `json:"severity"` + Source string `json:"source"` +} + +type Client struct { + options Options +} + +func New(options Options) *Client { + return &Client{options: options} +} + +func (c *Client) Trigger(runtime cre.Runtime, alert Alert) (int, error) { + sendAlert := func(config Options, nodeRuntime cre.NodeRuntime) (int, error) { + secret, err := nodeRuntime.GetSecret(&cre.SecretRequest{Id: config.RoutingKeySecret}).Await() + if err != nil { + return 0, fmt.Errorf("failed to get routing key: %w", err) + } + + payload := map[string]any{ + "routing_key": secret.Value, + "event_action": "trigger", + "payload": alert, + } + + body, err := json.Marshal(payload) + if err != nil { + return 0, fmt.Errorf("failed to marshal payload: %w", err) + } + + client := &http.Client{} + resp, err := client.SendRequest(nodeRuntime, &http.Request{ + Url: "https://events.pagerduty.com/v2/enqueue", + Method: "POST", + MultiHeaders: map[string]*http.HeaderValues{ + "Content-Type": {Values: []string{"application/json"}}, + }, + Body: body, + // Prevents every node from firing a duplicate alert + CacheSettings: &http.CacheSettings{Store: true}, + }).Await() + if err != nil { + return 0, fmt.Errorf("PagerDuty request failed: %w", err) + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return 0, fmt.Errorf("PagerDuty request failed with status %d", resp.StatusCode) + } + + return int(resp.StatusCode), nil + } + + promise := cre.RunInNodeMode(c.options, runtime, sendAlert, cre.ConsensusIdenticalAggregation[int]()) + return promise.Await() +} +``` + +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). +- Non-idempotent calls (POST/PUT/PATCH/DELETE) should set `CacheSettings` so a single DON-wide action isn't repeated once per node. +- Wrap non-deterministic work (HTTP, secrets) in `cre.RunInNodeMode(...)` with an explicit [consensus aggregation](/cre/reference/sdk/consensus-go) (`cre.ConsensusIdenticalAggregation`, `cre.ConsensusMedianAggregation`, or `cre.ConsensusAggregationFromTags` for a tagged result struct) so the DON agrees on one result before your function returns control to the workflow. +- `Body` on `http.Request` is a plain `[]byte` — unlike the TypeScript SDK, there's no base64-encoding step to worry about. +- All SDK capability calls return a [`Promise[T]`](/cre/reference/sdk/core-go#promise) that you resolve with `.Await()` — see the next section. + +## Pitfall 3: `Promise`/`Await` concurrency, not goroutines + +The Go SDK doesn't use `async`/`await` keywords — it uses a `Promise[T]` type with `.Await()`, `cre.Then()`, and `cre.ThenPromise()` for chaining (see [Core SDK Reference](/cre/reference/sdk/core-go#promise)). This exists because the underlying operation only actually runs when you call `.Await()` — building a promise chain without awaiting it does nothing. + +The pitfall specific to Go: don't reach for goroutines, channels, or `sync` primitives (`WaitGroup`, `Mutex`, and so on) to run multiple SDK calls "concurrently" inside your library. CRE workflows execute in a single-threaded WASM environment — using Go's concurrency primitives to fan out capability calls doesn't get you real parallelism, and a `select` across multiple channels picks a ready channel non-deterministically, which will cause consensus failures if the result depends on which one "won." + +- Never use `select` with multiple ready channels to decide which result to use or which branch to take — see [Avoiding Non-Determinism in Workflows](/cre/concepts/non-determinism-go#3-concurrency-and-channel-selection). +- If you need to make several capability calls, initiate them in a fixed order and resolve them (`.Await()`) in that same fixed order. You can build up several `Promise` values before awaiting any of them, but the order you await them in must never vary. +- `cre.Then()` / `cre.ThenPromise()` are the idiomatic way to chain dependent async steps without nested `.Await()` calls — prefer them over manually orchestrating goroutines. + +## Pitfall 4: determinism + +Every trigger callback in a CRE workflow runs independently on every DON node and must produce identical results (DON mode), except inside `cre.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. If your library does any of its own computation — not just HTTP or secrets — keep it deterministic: + +- **Never iterate a Go map directly** when the order affects your output. Go maps intentionally randomize iteration order. Use [`cre.OrderedEntries`](/cre/reference/sdk/core-go#creorderedentries-and-creorderedentriesfunc) (or `cre.OrderedEntriesFunc` for non-`cmp.Ordered` keys) instead of `for k, v := range someMap`. +- **Use `encoding/json` v1, not v2.** The v2 library uses randomized hashing for field ordering, which serializes the same struct differently across nodes. +- **Use `proto.MarshalOptions{Deterministic: true}.Marshal()`** if you serialize Protocol Buffers — the default `proto.Marshal` doesn't guarantee field order. +- **Never call `time.Now()` or any other `time` package clock function.** Accept the current time as a parameter (the workflow author gets it from `runtime.Now()`), or compute it inside a `RunInNodeMode` block. +- **Never use `math/rand` (or `crypto/rand`) directly.** Use `runtime.Rand()` from the CRE SDK, which provides a consensus-safe generator so every node produces the same sequence. +- **Never use `select` with multiple ready channels** to make a decision — see Pitfall 3 above. + + + + +## Pitfall 5: numeric precision for onchain and price values + +If your library produces or accepts values that will end up in a smart contract call — a price, an amount, a `uint256` — use `*big.Int`, never a plain `int`/`int64`/`float64`. For monetary or price values that aren't already onchain integers, use a fixed-point decimal type such as [`github.com/shopspring/decimal`](https://github.com/shopspring/decimal) rather than `float64`, which loses precision under repeated arithmetic. This matches the convention used throughout the CRE Go SDK's own examples. + +## Packaging your library + +Structure it as an ordinary Go module — there's no special CRE project layout for a library, only for a workflow (which is itself a package inside the consuming project's single Go module). + +``` +my-cre-library/ +├── go.mod +├── go.sum +├── pagerduty.go +└── README.md +``` + +**`go.mod`:** + +``` +module github.com/you/my-cre-library + +go 1.25.3 + +require github.com/smartcontractkit/cre-sdk-go v1.6.0 +``` + +Key points: + +- Use a normal `require` for `github.com/smartcontractkit/cre-sdk-go` — there's no `peerDependency` concept in Go modules, and you don't need one. Go's module resolution (minimal version selection) automatically unifies the SDK version across your library and the consuming workflow's `go.mod` to a single compatible version, so there's no risk of bundling two copies the way there is with npm. +- **Don't add a `//go:build wasip1` build tag to your library's own files** unless you have a specific reason to (for example, wrapping a WASI-only import). `cre.Runtime`, `cre.NodeRuntime`, and the capability client types are plain, portable Go — they compile under any `GOOS`. Only the workflow's own entry point (the file calling `wasm.NewRunner`) needs the `wasip1` tag. Keeping your library tag-free means your library's own `go test` suite — and any consumer's tests that exercise your library — run under the host's native `GOOS` instead of requiring a full WASM build. +- Take advantage of the SDK's mock packages where they exist (for example, `github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm/mock` for EVM calls) to unit test logic that depends on `cre.Runtime` without running `cre workflow simulate`. There is currently no equivalent mock package for the HTTP capability, so HTTP-calling code still needs an end-to-end `cre workflow simulate` pass to verify. +- Keep your own dependency tree small. Everything you import gets compiled 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 — or one with a dependency that unexpectedly requires cgo — can push an otherwise-fine workflow over its quota or break its WASM build entirely (cgo is not supported under `wasip1`). + +## Publishing + +Go modules don't have a central publish step or registry like npm — you publish by pushing your code to a Git repository (typically GitHub) and tagging a semver release: + +```bash +git tag v1.0.0 +git push origin v1.0.0 +``` + +Your module path (in `go.mod`) should match the repository's import path, for example `github.com/you/my-cre-library`. Nothing about tagging a release is CRE-specific — the workflow author's own `go build`/`cre workflow simulate` pipeline is what turns your published Go code into something that runs inside the WASM sandbox, on their side. + +## Using your library from a CRE workflow + +Recall that a Go CRE project is a single Go module — the workflow author adds your library with `go get` from the project root (not from inside the workflow subdirectory): + +```bash +go get github.com/you/my-cre-library@v1.0.0 +``` + +Then imports and calls it from their workflow handler, passing in the `runtime` they already have: + +```go +//go:build wasip1 + +package main + +import ( + "log/slog" + + "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron" + "github.com/smartcontractkit/cre-sdk-go/cre" + "github.com/smartcontractkit/cre-sdk-go/cre/wasm" + "github.com/you/my-cre-library" +) + +type Config struct { + Schedule string `json:"schedule"` +} + +var pagerDutyClient = pagerduty.New(pagerduty.Options{RoutingKeySecret: "pager-duty-routing-key"}) + +func onCronTrigger(config *Config, runtime cre.Runtime, trigger *cron.Payload) (string, error) { + _, err := pagerDutyClient.Trigger(runtime, pagerduty.Alert{ + Summary: "ETH/USD price breached threshold", + Severity: "critical", + Source: "cre-workflow", + }) + if err != nil { + return "", err + } + return "alert sent", nil +} + +func InitWorkflow(config *Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[*Config], error) { + return cre.Workflow[*Config]{ + cre.Handler(cron.Trigger(&cron.Config{Schedule: config.Schedule}), onCronTrigger), + }, nil +} + +func main() { + wasm.NewRunner(cre.ParseJSON[*Config]).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 + +- [ ] No filesystem access (`os.Open`, `os.ReadFile`, `os.WriteFile`, and similar) anywhere in your library or its dependencies. +- [ ] No direct network access (`net.Dial`, `net/http`, database drivers, gRPC dialing) — all outbound calls go through `http.Client`/`http.SendRequest` (or `evm.Client` for chain reads/writes). +- [ ] All non-deterministic work (HTTP, secrets, time, randomness) is wrapped in `cre.RunInNodeMode()` with an explicit consensus aggregation, or delegated back to the caller's `runtime`. +- [ ] No goroutines, channels, or `select` across multiple channels used to make a decision that affects your function's output. +- [ ] No direct map iteration where order affects the output — use `cre.OrderedEntries`/`cre.OrderedEntriesFunc`. +- [ ] `encoding/json` v1 (not v2) and, if applicable, `proto.MarshalOptions{Deterministic: true}` for serialization. +- [ ] No `time.Now()`, `math/rand`, or `crypto/rand` — use `runtime.Now()` and `runtime.Rand()`. +- [ ] `*big.Int` for onchain integer values; a decimal type (not `float64`) for prices and other precision-sensitive values. +- [ ] Secrets are accepted as an ID/name, never as a literal value baked into your library. +- [ ] No `//go:build wasip1` tag on library files unless truly required — keep your library testable with plain `go test`. +- [ ] Verified end-to-end with `cre workflow simulate` in a real (throwaway) CRE workflow that depends on your published or local module. + +## Learn more + +- [Avoiding Non-Determinism in Workflows](/cre/concepts/non-determinism-go): The complete determinism guide +- [SDK Reference: HTTP Client](/cre/reference/sdk/http-client-go): Full `http.Client` API +- [SDK Reference: Core](/cre/reference/sdk/core-go): `Runtime`, `NodeRuntime`, `Promise`, and `cre.OrderedEntries` +- [SDK Reference: Consensus & Aggregation](/cre/reference/sdk/consensus-go): Aggregation functions for `cre.RunInNodeMode` +- [Secrets](/cre/guides/workflow/secrets): Storing and retrieving secrets for a deployed workflow + +--- + # Deploying to the Onchain Registry Source: https://docs.chain.link/cre/guides/operations/deploying-to-onchain-registry-go Last Updated: 2026-05-12 diff --git a/src/content/cre/llms-full-ts.txt b/src/content/cre/llms-full-ts.txt index a3056a24fb1..90705f92750 100644 --- a/src/content/cre/llms-full-ts.txt +++ b/src/content/cre/llms-full-ts.txt @@ -12569,6 +12569,304 @@ You've now mastered the complete CRE development workflow! --- +# Building a Reusable Library +Source: https://docs.chain.link/cre/guides/operations/building-a-library-ts +Last Updated: 2026-08-14 + +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 is not a guide to writing a workflow itself — it assumes you're already familiar with that. 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. + +## The core constraint: your library runs where the workflow runs + +A CRE TypeScript workflow isn't run by Node.js. It's 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 — 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. + +That means: + +- 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 this from the start. Don't write it as a generic Node or browser package and hope it happens to work — write it against the CRE SDK's runtime primitives. + +## Pitfall 1: No Node.js built-ins + +QuickJS provides standard ECMAScript (`Map`, `Set`, `Promise`, most ES2020+ syntax) but not the Node.js API surface. The following are unavailable and will fail at compile or runtime if your library — or one of its transitive dependencies — imports them: + +``` +fs, path, crypto, process, http, https, net, stream, child_process, +os, worker_threads, cluster, dgram, dns, tls, vm, zlib, readline, +events (Node's EventEmitter), util (Node's promisify, etc.) +``` + +Practical implications: + +- **`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 documentation 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 — don't work 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`.** Never read configuration or credentials this way — there is no process. Accept configuration through your library's function arguments, and read secrets with `runtime.getSecret()` (see below). +- **No Node `crypto`.** If you need hashing or signing, use a pure-JS library with no native bindings. +- **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 — 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 — see [Pitfall 2](#pitfall-2-no-standard-http-client--use-the-cre-sdks-http-capability). 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 aren't 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) | See the note above. | +| `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. | + + + + +## Pitfall 2: No standard HTTP client — use the CRE SDK's HTTP capability + +Do not depend on `axios`, `node-fetch`, `ws`, or any HTTP or socket library — they all sit on top of Node's `http`/`net`/`stream` modules and will not work. There is also 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`. This isn't just a technical requirement — it's 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, even if it somehow ran. + +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. Your library is a thin, well-tested wrapper around this pattern — not a replacement for it. + +```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, alert: PagerDutyAlert): { statusCode: number } { + const sendAlert = (nodeRuntime: NodeRuntime): { 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). +- Non-idempotent calls (POST/PUT/PATCH/DELETE) should set `cacheSettings` so a single DON-wide action isn't repeated once per node. +- Wrap non-deterministic work (HTTP, secrets) 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. + +## Pitfall 3: 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) don't 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 — 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). + +## Pitfall 4: 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. If your library does any of its own computation — not just HTTP or secrets — keep it deterministic: + +- **Never call `Date.now()` or `new Date()`** inside DON-mode code — accept the current time as a parameter (the workflow author gets it from `runtime.now()`), or compute it 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. What's unsafe is randomness computed outside that runtime — for example a pre-computed value passed in, or a dependency that ships its own PRNG or native random source — since that won't go through CRE's override and will diverge across nodes. This randomness is also **not cryptographically secure** — don't use it in your library for anything security-sensitive, such as key generation or signing nonces. +- **Never use `Promise.race()`, `Promise.any()`, or `Promise.all()` with unpredictable or non-fixed ordering** around `.result()` calls. `Promise.race()`/`Promise.any()` let different nodes "win" with different sources; unpredictable `Promise.all()` usage has the same failure mode if the set or order of promises isn't fixed ahead of time. Instead, call `.result()` on each operation in a fixed, hardcoded order — 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 order you resolve them in never varies. +- **Never iterate plain `Object` keys with `for...in`** when order affects the result — object key order isn't guaranteed by spec, even though engines usually preserve insertion order. Use `Object.keys(obj).sort()` for guaranteed-deterministic order. `Map` and `Set` are safe to iterate directly since they guarantee insertion order by specification — prefer them over plain objects when your library's output depends on element order. + + + + +## Pitfall 5: numeric precision for onchain values + +If your library produces or accepts values that will end up in a smart contract call — a price, an amount, 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. + +``` +my-cre-library/ +├── src/ +│ └── index.ts +├── package.json +├── tsconfig.json +└── README.md +``` + +**`package.json`:** + +```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 — it needs it to run at all — 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`** (mirrors the workflow's own compiler settings so output stays compatible): + +```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, on their side. + +## 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): 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() + 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 + +- [ ] 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. +- [ ] All outbound network calls go through `HTTPClient` (or `EVMClient` for chain reads/writes), never a third-party HTTP or socket library. +- [ ] All non-deterministic work (HTTP, secrets, time, randomness) is wrapped in `runtime.runInNodeMode()` with an explicit consensus aggregation, or delegated back to the caller's `runtime`. +- [ ] No `Promise.race()`, `Promise.any()`, or order-unpredictable `Promise.all()` around `.result()` calls — resolution order is always fixed. +- [ ] No plain-object `for...in` iteration where key order affects the output — sorted `Object.keys()`, or `Map`/`Set`, instead. +- [ ] No `async` public API wrapping SDK capability calls — expose the synchronous `.result()`-based pattern. +- [ ] Secrets are accepted as an ID/name, never as a literal value baked into your library. +- [ ] `@chainlink/cre-sdk` is a `peerDependency`, not a bundled dependency. +- [ ] Verified end-to-end with `cre workflow simulate` in a real (throwaway) CRE workflow that depends on your published or local package. + +## 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 + +--- + # Deploying to the Onchain Registry Source: https://docs.chain.link/cre/guides/operations/deploying-to-onchain-registry-ts Last Updated: 2026-05-12 From e6942c41685637577a817d33a17838fd054e8a16 Mon Sep 17 00:00:00 2001 From: Emmanuel Jacquier Date: Fri, 14 Aug 2026 08:51:36 -0400 Subject: [PATCH 2/6] Updated sidebar --- src/config/sidebar.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/config/sidebar.ts b/src/config/sidebar.ts index 5089f583b70..5b14ce0ab28 100644 --- a/src/config/sidebar.ts +++ b/src/config/sidebar.ts @@ -495,6 +495,14 @@ export const SIDEBAR: Partial> = { url: "cre/guides/workflow/using-randomness", highlightAsCurrent: ["cre/guides/workflow/using-randomness-ts", "cre/guides/workflow/using-randomness-go"], }, + { + title: "Building a Reusable Library", + url: "cre/guides/operations/building-a-library", + highlightAsCurrent: [ + "cre/guides/operations/building-a-library-ts", + "cre/guides/operations/building-a-library-go", + ], + }, ], }, { From 93f1b8df9a5ab5816abb8953d4a39c1f22fc90eb Mon Sep 17 00:00:00 2001 From: Emmanuel Jacquier Date: Mon, 17 Aug 2026 07:10:32 -0400 Subject: [PATCH 3/6] updated library guide menu --- src/config/sidebar.ts | 6 +- .../building-a-library-go.mdx | 0 .../building-a-library-ts.mdx | 0 src/content/cre/llms-full-go.txt | 758 +++++----- src/content/cre/llms-full-ts.txt | 1278 ++++++++--------- 5 files changed, 1021 insertions(+), 1021 deletions(-) rename src/content/cre/guides/{operations => workflow}/building-a-library-go.mdx (100%) rename src/content/cre/guides/{operations => workflow}/building-a-library-ts.mdx (100%) diff --git a/src/config/sidebar.ts b/src/config/sidebar.ts index 5b14ce0ab28..6e0f0c22ce5 100644 --- a/src/config/sidebar.ts +++ b/src/config/sidebar.ts @@ -497,10 +497,10 @@ export const SIDEBAR: Partial> = { }, { title: "Building a Reusable Library", - url: "cre/guides/operations/building-a-library", + url: "cre/guides/workflow/building-a-library", highlightAsCurrent: [ - "cre/guides/operations/building-a-library-ts", - "cre/guides/operations/building-a-library-go", + "cre/guides/workflow/building-a-library-ts", + "cre/guides/workflow/building-a-library-go", ], }, ], diff --git a/src/content/cre/guides/operations/building-a-library-go.mdx b/src/content/cre/guides/workflow/building-a-library-go.mdx similarity index 100% rename from src/content/cre/guides/operations/building-a-library-go.mdx rename to src/content/cre/guides/workflow/building-a-library-go.mdx diff --git a/src/content/cre/guides/operations/building-a-library-ts.mdx b/src/content/cre/guides/workflow/building-a-library-ts.mdx similarity index 100% rename from src/content/cre/guides/operations/building-a-library-ts.mdx rename to src/content/cre/guides/workflow/building-a-library-ts.mdx diff --git a/src/content/cre/llms-full-go.txt b/src/content/cre/llms-full-go.txt index d639231b245..47579437866 100644 --- a/src/content/cre/llms-full-go.txt +++ b/src/content/cre/llms-full-go.txt @@ -12389,285 +12389,6 @@ You've now mastered the complete CRE development workflow! --- -# Building a Reusable Library -Source: https://docs.chain.link/cre/guides/operations/building-a-library-go -Last Updated: 2026-08-14 - -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 Go module that other developers can `go get` and import directly into their own CRE workflows. - -This is not a guide to writing a workflow itself — it assumes you're already familiar with that. 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 Go module that other people's CRE workflows will depend on. - -## The core constraint: your library runs where the workflow runs - -A CRE Go workflow isn't run as a normal Go binary. It's cross-compiled with `GOOS=wasip1 GOARCH=wasm` into a WebAssembly module that runs inside the CRE host's WASI sandbox. When a workflow author adds your module as a dependency, your code is compiled into that same binary — it runs under the exact same restrictions as the workflow's own code. - -Unlike the TypeScript SDK, which runs inside a stripped-down QuickJS engine, Go workflows compile with the **full Go standard library** available at compile time. This means far more of the ecosystem "just works" without compatibility checking — but the WASI sandbox still enforces real limits at runtime: - -- No filesystem access. There's no real disk backing the sandbox, so file I/O fails or is meaningless even though the code compiles. -- No arbitrary outbound network access. There's no raw socket layer to dial into — everything non-deterministic (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. -- Single-threaded, deterministic execution. All DON nodes must execute your code identically and produce the same output. - -Design your library around this from the start. Don't write it as a generic Go module that happens to shell out to the filesystem or network and hope it works under WASI — write it against the CRE SDK's runtime primitives. - -## Pitfall 1: No filesystem or arbitrary network access - -Your library (or one of its transitive dependencies) must not depend on: - -- **Filesystem access** — `os.Open`, `os.ReadFile`, `os.WriteFile`, config-file loaders, embedded file caches written to disk, and so on. There is no writable (or meaningfully readable) filesystem in the WASI sandbox the workflow runs in. -- **Direct network access** — `net.Dial`, `net/http`'s `http.Client`/`http.Get`, gRPC dialing, database drivers, or any other package that opens its own socket. These either fail at runtime or, worse, silently do nothing useful, because there's no outbound network stack available to your code directly — only to the CRE host, through capability calls. - -Anything your library needs from disk (default config, lookup tables, certificates) should be compiled in as Go constants, embedded with `//go:embed` (a compile-time read, not a runtime filesystem access — this is safe), or passed in as arguments by the caller. Anything it needs from the network must go through `http.Client`/`http.SendRequest` from `@chainlink/cre-sdk`'s Go equivalent, [`cre-sdk-go`](#pitfall-2-no-standard-http-client--use-the-cre-sdks-http-capability) — never a raw dependency that dials sockets itself. - - - - -## Pitfall 2: No standard HTTP client — use the CRE SDK's HTTP capability - -Do not depend on `net/http`, a REST client wrapper built on it, or any package that dials its own connections. Instead, all outbound requests must go through [`http.Client`](/cre/reference/sdk/http-client-go) from `github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http`. This isn't just a technical requirement — it's 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 dials out with `net/http` would bypass this consensus mechanism entirely, even if it somehow ran. - -Consequence for library design: your exported functions should take a `cre.Runtime` as a parameter, and use it (via `http.SendRequest` or `cre.RunInNodeMode`) to construct requests, exactly as a workflow author would inline. Your library is a thin, well-tested wrapper around this pattern — not a replacement for it. - -```go -// pagerduty.go — inside your library module -package pagerduty - -import ( - "encoding/json" - "fmt" - - "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http" - "github.com/smartcontractkit/cre-sdk-go/cre" -) - -type Options struct { - RoutingKeySecret string -} - -type Alert struct { - Summary string `json:"summary"` - Severity string `json:"severity"` - Source string `json:"source"` -} - -type Client struct { - options Options -} - -func New(options Options) *Client { - return &Client{options: options} -} - -func (c *Client) Trigger(runtime cre.Runtime, alert Alert) (int, error) { - sendAlert := func(config Options, nodeRuntime cre.NodeRuntime) (int, error) { - secret, err := nodeRuntime.GetSecret(&cre.SecretRequest{Id: config.RoutingKeySecret}).Await() - if err != nil { - return 0, fmt.Errorf("failed to get routing key: %w", err) - } - - payload := map[string]any{ - "routing_key": secret.Value, - "event_action": "trigger", - "payload": alert, - } - - body, err := json.Marshal(payload) - if err != nil { - return 0, fmt.Errorf("failed to marshal payload: %w", err) - } - - client := &http.Client{} - resp, err := client.SendRequest(nodeRuntime, &http.Request{ - Url: "https://events.pagerduty.com/v2/enqueue", - Method: "POST", - MultiHeaders: map[string]*http.HeaderValues{ - "Content-Type": {Values: []string{"application/json"}}, - }, - Body: body, - // Prevents every node from firing a duplicate alert - CacheSettings: &http.CacheSettings{Store: true}, - }).Await() - if err != nil { - return 0, fmt.Errorf("PagerDuty request failed: %w", err) - } - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return 0, fmt.Errorf("PagerDuty request failed with status %d", resp.StatusCode) - } - - return int(resp.StatusCode), nil - } - - promise := cre.RunInNodeMode(c.options, runtime, sendAlert, cre.ConsensusIdenticalAggregation[int]()) - return promise.Await() -} -``` - -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). -- Non-idempotent calls (POST/PUT/PATCH/DELETE) should set `CacheSettings` so a single DON-wide action isn't repeated once per node. -- Wrap non-deterministic work (HTTP, secrets) in `cre.RunInNodeMode(...)` with an explicit [consensus aggregation](/cre/reference/sdk/consensus-go) (`cre.ConsensusIdenticalAggregation`, `cre.ConsensusMedianAggregation`, or `cre.ConsensusAggregationFromTags` for a tagged result struct) so the DON agrees on one result before your function returns control to the workflow. -- `Body` on `http.Request` is a plain `[]byte` — unlike the TypeScript SDK, there's no base64-encoding step to worry about. -- All SDK capability calls return a [`Promise[T]`](/cre/reference/sdk/core-go#promise) that you resolve with `.Await()` — see the next section. - -## Pitfall 3: `Promise`/`Await` concurrency, not goroutines - -The Go SDK doesn't use `async`/`await` keywords — it uses a `Promise[T]` type with `.Await()`, `cre.Then()`, and `cre.ThenPromise()` for chaining (see [Core SDK Reference](/cre/reference/sdk/core-go#promise)). This exists because the underlying operation only actually runs when you call `.Await()` — building a promise chain without awaiting it does nothing. - -The pitfall specific to Go: don't reach for goroutines, channels, or `sync` primitives (`WaitGroup`, `Mutex`, and so on) to run multiple SDK calls "concurrently" inside your library. CRE workflows execute in a single-threaded WASM environment — using Go's concurrency primitives to fan out capability calls doesn't get you real parallelism, and a `select` across multiple channels picks a ready channel non-deterministically, which will cause consensus failures if the result depends on which one "won." - -- Never use `select` with multiple ready channels to decide which result to use or which branch to take — see [Avoiding Non-Determinism in Workflows](/cre/concepts/non-determinism-go#3-concurrency-and-channel-selection). -- If you need to make several capability calls, initiate them in a fixed order and resolve them (`.Await()`) in that same fixed order. You can build up several `Promise` values before awaiting any of them, but the order you await them in must never vary. -- `cre.Then()` / `cre.ThenPromise()` are the idiomatic way to chain dependent async steps without nested `.Await()` calls — prefer them over manually orchestrating goroutines. - -## Pitfall 4: determinism - -Every trigger callback in a CRE workflow runs independently on every DON node and must produce identical results (DON mode), except inside `cre.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. If your library does any of its own computation — not just HTTP or secrets — keep it deterministic: - -- **Never iterate a Go map directly** when the order affects your output. Go maps intentionally randomize iteration order. Use [`cre.OrderedEntries`](/cre/reference/sdk/core-go#creorderedentries-and-creorderedentriesfunc) (or `cre.OrderedEntriesFunc` for non-`cmp.Ordered` keys) instead of `for k, v := range someMap`. -- **Use `encoding/json` v1, not v2.** The v2 library uses randomized hashing for field ordering, which serializes the same struct differently across nodes. -- **Use `proto.MarshalOptions{Deterministic: true}.Marshal()`** if you serialize Protocol Buffers — the default `proto.Marshal` doesn't guarantee field order. -- **Never call `time.Now()` or any other `time` package clock function.** Accept the current time as a parameter (the workflow author gets it from `runtime.Now()`), or compute it inside a `RunInNodeMode` block. -- **Never use `math/rand` (or `crypto/rand`) directly.** Use `runtime.Rand()` from the CRE SDK, which provides a consensus-safe generator so every node produces the same sequence. -- **Never use `select` with multiple ready channels** to make a decision — see Pitfall 3 above. - - - - -## Pitfall 5: numeric precision for onchain and price values - -If your library produces or accepts values that will end up in a smart contract call — a price, an amount, a `uint256` — use `*big.Int`, never a plain `int`/`int64`/`float64`. For monetary or price values that aren't already onchain integers, use a fixed-point decimal type such as [`github.com/shopspring/decimal`](https://github.com/shopspring/decimal) rather than `float64`, which loses precision under repeated arithmetic. This matches the convention used throughout the CRE Go SDK's own examples. - -## Packaging your library - -Structure it as an ordinary Go module — there's no special CRE project layout for a library, only for a workflow (which is itself a package inside the consuming project's single Go module). - -``` -my-cre-library/ -├── go.mod -├── go.sum -├── pagerduty.go -└── README.md -``` - -**`go.mod`:** - -``` -module github.com/you/my-cre-library - -go 1.25.3 - -require github.com/smartcontractkit/cre-sdk-go v1.6.0 -``` - -Key points: - -- Use a normal `require` for `github.com/smartcontractkit/cre-sdk-go` — there's no `peerDependency` concept in Go modules, and you don't need one. Go's module resolution (minimal version selection) automatically unifies the SDK version across your library and the consuming workflow's `go.mod` to a single compatible version, so there's no risk of bundling two copies the way there is with npm. -- **Don't add a `//go:build wasip1` build tag to your library's own files** unless you have a specific reason to (for example, wrapping a WASI-only import). `cre.Runtime`, `cre.NodeRuntime`, and the capability client types are plain, portable Go — they compile under any `GOOS`. Only the workflow's own entry point (the file calling `wasm.NewRunner`) needs the `wasip1` tag. Keeping your library tag-free means your library's own `go test` suite — and any consumer's tests that exercise your library — run under the host's native `GOOS` instead of requiring a full WASM build. -- Take advantage of the SDK's mock packages where they exist (for example, `github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm/mock` for EVM calls) to unit test logic that depends on `cre.Runtime` without running `cre workflow simulate`. There is currently no equivalent mock package for the HTTP capability, so HTTP-calling code still needs an end-to-end `cre workflow simulate` pass to verify. -- Keep your own dependency tree small. Everything you import gets compiled 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 — or one with a dependency that unexpectedly requires cgo — can push an otherwise-fine workflow over its quota or break its WASM build entirely (cgo is not supported under `wasip1`). - -## Publishing - -Go modules don't have a central publish step or registry like npm — you publish by pushing your code to a Git repository (typically GitHub) and tagging a semver release: - -```bash -git tag v1.0.0 -git push origin v1.0.0 -``` - -Your module path (in `go.mod`) should match the repository's import path, for example `github.com/you/my-cre-library`. Nothing about tagging a release is CRE-specific — the workflow author's own `go build`/`cre workflow simulate` pipeline is what turns your published Go code into something that runs inside the WASM sandbox, on their side. - -## Using your library from a CRE workflow - -Recall that a Go CRE project is a single Go module — the workflow author adds your library with `go get` from the project root (not from inside the workflow subdirectory): - -```bash -go get github.com/you/my-cre-library@v1.0.0 -``` - -Then imports and calls it from their workflow handler, passing in the `runtime` they already have: - -```go -//go:build wasip1 - -package main - -import ( - "log/slog" - - "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron" - "github.com/smartcontractkit/cre-sdk-go/cre" - "github.com/smartcontractkit/cre-sdk-go/cre/wasm" - "github.com/you/my-cre-library" -) - -type Config struct { - Schedule string `json:"schedule"` -} - -var pagerDutyClient = pagerduty.New(pagerduty.Options{RoutingKeySecret: "pager-duty-routing-key"}) - -func onCronTrigger(config *Config, runtime cre.Runtime, trigger *cron.Payload) (string, error) { - _, err := pagerDutyClient.Trigger(runtime, pagerduty.Alert{ - Summary: "ETH/USD price breached threshold", - Severity: "critical", - Source: "cre-workflow", - }) - if err != nil { - return "", err - } - return "alert sent", nil -} - -func InitWorkflow(config *Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[*Config], error) { - return cre.Workflow[*Config]{ - cre.Handler(cron.Trigger(&cron.Config{Schedule: config.Schedule}), onCronTrigger), - }, nil -} - -func main() { - wasm.NewRunner(cre.ParseJSON[*Config]).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 - -- [ ] No filesystem access (`os.Open`, `os.ReadFile`, `os.WriteFile`, and similar) anywhere in your library or its dependencies. -- [ ] No direct network access (`net.Dial`, `net/http`, database drivers, gRPC dialing) — all outbound calls go through `http.Client`/`http.SendRequest` (or `evm.Client` for chain reads/writes). -- [ ] All non-deterministic work (HTTP, secrets, time, randomness) is wrapped in `cre.RunInNodeMode()` with an explicit consensus aggregation, or delegated back to the caller's `runtime`. -- [ ] No goroutines, channels, or `select` across multiple channels used to make a decision that affects your function's output. -- [ ] No direct map iteration where order affects the output — use `cre.OrderedEntries`/`cre.OrderedEntriesFunc`. -- [ ] `encoding/json` v1 (not v2) and, if applicable, `proto.MarshalOptions{Deterministic: true}` for serialization. -- [ ] No `time.Now()`, `math/rand`, or `crypto/rand` — use `runtime.Now()` and `runtime.Rand()`. -- [ ] `*big.Int` for onchain integer values; a decimal type (not `float64`) for prices and other precision-sensitive values. -- [ ] Secrets are accepted as an ID/name, never as a literal value baked into your library. -- [ ] No `//go:build wasip1` tag on library files unless truly required — keep your library testable with plain `go test`. -- [ ] Verified end-to-end with `cre workflow simulate` in a real (throwaway) CRE workflow that depends on your published or local module. - -## Learn more - -- [Avoiding Non-Determinism in Workflows](/cre/concepts/non-determinism-go): The complete determinism guide -- [SDK Reference: HTTP Client](/cre/reference/sdk/http-client-go): Full `http.Client` API -- [SDK Reference: Core](/cre/reference/sdk/core-go): `Runtime`, `NodeRuntime`, `Promise`, and `cre.OrderedEntries` -- [SDK Reference: Consensus & Aggregation](/cre/reference/sdk/consensus-go): Aggregation functions for `cre.RunInNodeMode` -- [Secrets](/cre/guides/workflow/secrets): Storing and retrieving secrets for a deployed workflow - ---- - # Deploying to the Onchain Registry Source: https://docs.chain.link/cre/guides/operations/deploying-to-onchain-registry-go Last Updated: 2026-05-12 @@ -13205,160 +12926,439 @@ Update your `GATEWAY_URL` (or equivalent configuration) wherever you send trigge See [Triggering Deployed Workflows](/cre/guides/workflow/using-triggers/http-trigger/triggering-deployed-workflows) for the full HTTP trigger request format and authentication details. -## CI/CD with the private registry +## CI/CD with the private registry + +Because the private registry does not require `CRE_ETH_PRIVATE_KEY` or an Ethereum Mainnet RPC, CI/CD pipelines are simpler. You only need `CRE_API_KEY`: + +```yaml +# .github/workflows/deploy-private.yml +name: Deploy to Private Registry + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install CRE CLI + run: curl -sSL https://github.com/smartcontractkit/cre-cli/releases/latest/download/install.sh | sh + + - name: Deploy workflow + run: cre workflow deploy ./my-workflow --target staging-settings --yes --non-interactive + env: + CRE_API_KEY: ${{ secrets.CRE_API_KEY }} +``` + + + + +## Next steps + +- [Deploying Workflows](/cre/guides/operations/deploying-workflows) — Registry overview and comparison +- [Deploying to the Onchain Registry](/cre/guides/operations/deploying-to-onchain-registry) — Deploy with a linked wallet key and onchain record +- [Activating & Pausing Workflows](/cre/guides/operations/activating-pausing-workflows) — Control workflow execution state +- [Using Secrets with Deployed Workflows](/cre/guides/workflow/secrets/using-secrets-deployed) — Manage secrets for deployed workflows +- [Registry Commands Reference](/cre/reference/cli/registry) — `cre registry list` and related commands + +--- + +# Verifying Workflows +Source: https://docs.chain.link/cre/guides/operations/verifying-workflows-go +Last Updated: 2026-04-09 + + + +Workflow verification ensures the integrity and authenticity of your workflows across deployment, onchain execution, and third-party auditing. This guide explains how workflow IDs are computed, how to verify workflows in consumer contracts, and how to enable independent verification by third parties. + +## Workflow ID + +The workflow ID is a unique hash that serves as the primary identifier for your workflow throughout its lifecycle. It is computed locally during [`cre workflow deploy`](/cre/reference/cli/workflow#cre-workflow-deploy) from the following inputs: + +- **workflowOwner**: The deployer's address +- **workflowName**: The name specified in your workflow +- **Compiled workflow binary**: The WASM binary produced from your workflow code +- **Config file contents**: The contents of your workflow's config file +- **Secrets hash**: An empty string placeholder for secrets + +Because the workflow ID is derived from these inputs, it deterministically represents a specific version of your workflow code and configuration. + + + +Use [`cre workflow hash`](/cre/reference/cli/workflow#cre-workflow-hash) to inspect the workflow ID before deploying. This lets you preview the ID without submitting an onchain transaction. + +For more details on deployment and updates, see [Deploying Workflows](/cre/guides/operations/deploying-workflows) and [Updating Deployed Workflows](/cre/guides/operations/updating-deployed-workflows). + +## Verifying workflows onchain + +When a workflow writes onchain, the consumer contract receives both the report data and metadata through the `onReport` callback. The metadata contains information you can use to verify the source of the report: + +- **`workflowId`** (`bytes32`): The unique workflow hash +- **`workflowName`** (`bytes10`): The workflow name, hash-encoded +- **`workflowOwner`** (`address`): The address that deployed the workflow + +See [Building Consumer Contracts](/cre/guides/workflow/using-evm-client/onchain-write/building-consumer-contracts) for the full `IReceiver` interface and metadata structure. + +### Workflow name encoding + +The `workflowName` in metadata is not stored as a plain string. It is a SHA256 hash of the workflow name, truncated to `bytes10`. See [how workflow names are encoded](/cre/guides/workflow/using-evm-client/onchain-write/building-consumer-contracts#how-workflow-names-are-encoded) for the full encoding process. + +### Security best practices + +Follow these practices to ensure only authorized workflows can interact with your consumer contract: + +- **Verify `msg.sender`**: Always check that `msg.sender` is the expected forwarder address. See the [Forwarder Directory](/cre/guides/workflow/using-evm-client/forwarder-directory) for addresses by network. +- **Permission on workflow ID**: Use `setExpectedWorkflowId` from `ReceiverTemplate` to restrict which workflow can call your contract. + + + +## Third-party verification + +Third-party verification allows customers or auditors to independently confirm that a deployed workflow matches its source code. The deployer shares the workflow source, and the verifier uses the CRE CLI to compute the workflow hash and compare it against the onchain workflow ID. + +Go workflows are verifiable by default and do not require a template or Docker. + +### Steps for the workflow developer + +1. **Add a `.env.public` file** to your workflow folder with `GOTOOLCHAIN` set to the Go toolchain version you use to build the workflow. Pinning that version helps reproducible builds across machines and environments. Add this file *before* running `cre workflow deploy`. + + Example (replace with your own version—the tag below is not prescriptive): + + ``` + GOTOOLCHAIN=go1.23.0 + ``` + + Use the same toolchain string you build with; `go version` reports it (for example `go1.23.0 linux/amd64` → use `go1.23.0`). + +2. **Share your workflow source** with the verifier. Provide a zip archive or repository link that includes all workflow files, including `.env.public`. Exclude `.env` files that contain private keys or secrets. + +### Steps for the verifier + +1. [**Install the CRE CLI**](/cre/getting-started/cli-installation). No login or deploy access is required for hash verification. + +2. **Unzip or clone** the shared workflow repository. + +3. **Run `cre workflow hash`** to compute the workflow hash: + + ```bash + cre workflow hash ./workflow-folder --public_key 0xYourDeployerAddress + ``` + + Replace `./workflow-folder` with the path to the workflow source and `0xYourDeployerAddress` with the deployer's public address. + +4. **Compare the output** with the workflow ID observed onchain. The `Workflow hash` value in the output corresponds to the onchain workflow ID: + + ``` + Binary hash: 0dcbb19de3c22edfe61605a970eb6d42199df91ac3e992cd3f2e33cb13efbb4c + Config hash: 3bdaebcc2f639d77cb248242c1d01c8651f540cdbf423d26fe3128516fd225b6 + Workflow hash: 004fff5bb1ae05cc16e453f8ad564f5e8b0eae1945ec22f3d0adfc0339954d56 + ``` + + If the workflow hash matches the onchain workflow ID, the deployed workflow matches the shared source code. + + + +## Learn more + +- [Deploying Workflows](/cre/guides/operations/deploying-workflows) +- [Updating Deployed Workflows](/cre/guides/operations/updating-deployed-workflows) +- [Building Consumer Contracts](/cre/guides/workflow/using-evm-client/onchain-write/building-consumer-contracts) +- [CRE CLI Workflow Reference](/cre/reference/cli/workflow) + +--- + +# Building a Reusable Library +Source: https://docs.chain.link/cre/guides/workflow/building-a-library-go +Last Updated: 2026-08-14 + +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 Go module that other developers can `go get` and import directly into their own CRE workflows. + +This is not a guide to writing a workflow itself — it assumes you're already familiar with that. 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 Go module that other people's CRE workflows will depend on. + +## The core constraint: your library runs where the workflow runs + +A CRE Go workflow isn't run as a normal Go binary. It's cross-compiled with `GOOS=wasip1 GOARCH=wasm` into a WebAssembly module that runs inside the CRE host's WASI sandbox. When a workflow author adds your module as a dependency, your code is compiled into that same binary — it runs under the exact same restrictions as the workflow's own code. + +Unlike the TypeScript SDK, which runs inside a stripped-down QuickJS engine, Go workflows compile with the **full Go standard library** available at compile time. This means far more of the ecosystem "just works" without compatibility checking — but the WASI sandbox still enforces real limits at runtime: + +- No filesystem access. There's no real disk backing the sandbox, so file I/O fails or is meaningless even though the code compiles. +- No arbitrary outbound network access. There's no raw socket layer to dial into — everything non-deterministic (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. +- Single-threaded, deterministic execution. All DON nodes must execute your code identically and produce the same output. + +Design your library around this from the start. Don't write it as a generic Go module that happens to shell out to the filesystem or network and hope it works under WASI — write it against the CRE SDK's runtime primitives. + +## Pitfall 1: No filesystem or arbitrary network access + +Your library (or one of its transitive dependencies) must not depend on: + +- **Filesystem access** — `os.Open`, `os.ReadFile`, `os.WriteFile`, config-file loaders, embedded file caches written to disk, and so on. There is no writable (or meaningfully readable) filesystem in the WASI sandbox the workflow runs in. +- **Direct network access** — `net.Dial`, `net/http`'s `http.Client`/`http.Get`, gRPC dialing, database drivers, or any other package that opens its own socket. These either fail at runtime or, worse, silently do nothing useful, because there's no outbound network stack available to your code directly — only to the CRE host, through capability calls. + +Anything your library needs from disk (default config, lookup tables, certificates) should be compiled in as Go constants, embedded with `//go:embed` (a compile-time read, not a runtime filesystem access — this is safe), or passed in as arguments by the caller. Anything it needs from the network must go through `http.Client`/`http.SendRequest` from `@chainlink/cre-sdk`'s Go equivalent, [`cre-sdk-go`](#pitfall-2-no-standard-http-client--use-the-cre-sdks-http-capability) — never a raw dependency that dials sockets itself. + + + + +## Pitfall 2: No standard HTTP client — use the CRE SDK's HTTP capability + +Do not depend on `net/http`, a REST client wrapper built on it, or any package that dials its own connections. Instead, all outbound requests must go through [`http.Client`](/cre/reference/sdk/http-client-go) from `github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http`. This isn't just a technical requirement — it's 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 dials out with `net/http` would bypass this consensus mechanism entirely, even if it somehow ran. + +Consequence for library design: your exported functions should take a `cre.Runtime` as a parameter, and use it (via `http.SendRequest` or `cre.RunInNodeMode`) to construct requests, exactly as a workflow author would inline. Your library is a thin, well-tested wrapper around this pattern — not a replacement for it. + +```go +// pagerduty.go — inside your library module +package pagerduty + +import ( + "encoding/json" + "fmt" + + "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http" + "github.com/smartcontractkit/cre-sdk-go/cre" +) + +type Options struct { + RoutingKeySecret string +} + +type Alert struct { + Summary string `json:"summary"` + Severity string `json:"severity"` + Source string `json:"source"` +} + +type Client struct { + options Options +} -Because the private registry does not require `CRE_ETH_PRIVATE_KEY` or an Ethereum Mainnet RPC, CI/CD pipelines are simpler. You only need `CRE_API_KEY`: +func New(options Options) *Client { + return &Client{options: options} +} -```yaml -# .github/workflows/deploy-private.yml -name: Deploy to Private Registry +func (c *Client) Trigger(runtime cre.Runtime, alert Alert) (int, error) { + sendAlert := func(config Options, nodeRuntime cre.NodeRuntime) (int, error) { + secret, err := nodeRuntime.GetSecret(&cre.SecretRequest{Id: config.RoutingKeySecret}).Await() + if err != nil { + return 0, fmt.Errorf("failed to get routing key: %w", err) + } -on: - push: - branches: [main] + payload := map[string]any{ + "routing_key": secret.Value, + "event_action": "trigger", + "payload": alert, + } -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 + body, err := json.Marshal(payload) + if err != nil { + return 0, fmt.Errorf("failed to marshal payload: %w", err) + } - - name: Install CRE CLI - run: curl -sSL https://github.com/smartcontractkit/cre-cli/releases/latest/download/install.sh | sh + client := &http.Client{} + resp, err := client.SendRequest(nodeRuntime, &http.Request{ + Url: "https://events.pagerduty.com/v2/enqueue", + Method: "POST", + MultiHeaders: map[string]*http.HeaderValues{ + "Content-Type": {Values: []string{"application/json"}}, + }, + Body: body, + // Prevents every node from firing a duplicate alert + CacheSettings: &http.CacheSettings{Store: true}, + }).Await() + if err != nil { + return 0, fmt.Errorf("PagerDuty request failed: %w", err) + } - - name: Deploy workflow - run: cre workflow deploy ./my-workflow --target staging-settings --yes --non-interactive - env: - CRE_API_KEY: ${{ secrets.CRE_API_KEY }} -``` + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return 0, fmt.Errorf("PagerDuty request failed with status %d", resp.StatusCode) + } + return int(resp.StatusCode), nil + } - + promise := cre.RunInNodeMode(c.options, runtime, sendAlert, cre.ConsensusIdenticalAggregation[int]()) + return promise.Await() +} +``` -## Next steps +Notes on this pattern: -- [Deploying Workflows](/cre/guides/operations/deploying-workflows) — Registry overview and comparison -- [Deploying to the Onchain Registry](/cre/guides/operations/deploying-to-onchain-registry) — Deploy with a linked wallet key and onchain record -- [Activating & Pausing Workflows](/cre/guides/operations/activating-pausing-workflows) — Control workflow execution state -- [Using Secrets with Deployed Workflows](/cre/guides/workflow/secrets/using-secrets-deployed) — Manage secrets for deployed workflows -- [Registry Commands Reference](/cre/reference/cli/registry) — `cre registry list` and related commands +- 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). +- Non-idempotent calls (POST/PUT/PATCH/DELETE) should set `CacheSettings` so a single DON-wide action isn't repeated once per node. +- Wrap non-deterministic work (HTTP, secrets) in `cre.RunInNodeMode(...)` with an explicit [consensus aggregation](/cre/reference/sdk/consensus-go) (`cre.ConsensusIdenticalAggregation`, `cre.ConsensusMedianAggregation`, or `cre.ConsensusAggregationFromTags` for a tagged result struct) so the DON agrees on one result before your function returns control to the workflow. +- `Body` on `http.Request` is a plain `[]byte` — unlike the TypeScript SDK, there's no base64-encoding step to worry about. +- All SDK capability calls return a [`Promise[T]`](/cre/reference/sdk/core-go#promise) that you resolve with `.Await()` — see the next section. ---- +## Pitfall 3: `Promise`/`Await` concurrency, not goroutines -# Verifying Workflows -Source: https://docs.chain.link/cre/guides/operations/verifying-workflows-go -Last Updated: 2026-04-09 +The Go SDK doesn't use `async`/`await` keywords — it uses a `Promise[T]` type with `.Await()`, `cre.Then()`, and `cre.ThenPromise()` for chaining (see [Core SDK Reference](/cre/reference/sdk/core-go#promise)). This exists because the underlying operation only actually runs when you call `.Await()` — building a promise chain without awaiting it does nothing. - +The pitfall specific to Go: don't reach for goroutines, channels, or `sync` primitives (`WaitGroup`, `Mutex`, and so on) to run multiple SDK calls "concurrently" inside your library. CRE workflows execute in a single-threaded WASM environment — using Go's concurrency primitives to fan out capability calls doesn't get you real parallelism, and a `select` across multiple channels picks a ready channel non-deterministically, which will cause consensus failures if the result depends on which one "won." -Workflow verification ensures the integrity and authenticity of your workflows across deployment, onchain execution, and third-party auditing. This guide explains how workflow IDs are computed, how to verify workflows in consumer contracts, and how to enable independent verification by third parties. +- Never use `select` with multiple ready channels to decide which result to use or which branch to take — see [Avoiding Non-Determinism in Workflows](/cre/concepts/non-determinism-go#3-concurrency-and-channel-selection). +- If you need to make several capability calls, initiate them in a fixed order and resolve them (`.Await()`) in that same fixed order. You can build up several `Promise` values before awaiting any of them, but the order you await them in must never vary. +- `cre.Then()` / `cre.ThenPromise()` are the idiomatic way to chain dependent async steps without nested `.Await()` calls — prefer them over manually orchestrating goroutines. -## Workflow ID +## Pitfall 4: determinism -The workflow ID is a unique hash that serves as the primary identifier for your workflow throughout its lifecycle. It is computed locally during [`cre workflow deploy`](/cre/reference/cli/workflow#cre-workflow-deploy) from the following inputs: +Every trigger callback in a CRE workflow runs independently on every DON node and must produce identical results (DON mode), except inside `cre.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. If your library does any of its own computation — not just HTTP or secrets — keep it deterministic: -- **workflowOwner**: The deployer's address -- **workflowName**: The name specified in your workflow -- **Compiled workflow binary**: The WASM binary produced from your workflow code -- **Config file contents**: The contents of your workflow's config file -- **Secrets hash**: An empty string placeholder for secrets +- **Never iterate a Go map directly** when the order affects your output. Go maps intentionally randomize iteration order. Use [`cre.OrderedEntries`](/cre/reference/sdk/core-go#creorderedentries-and-creorderedentriesfunc) (or `cre.OrderedEntriesFunc` for non-`cmp.Ordered` keys) instead of `for k, v := range someMap`. +- **Use `encoding/json` v1, not v2.** The v2 library uses randomized hashing for field ordering, which serializes the same struct differently across nodes. +- **Use `proto.MarshalOptions{Deterministic: true}.Marshal()`** if you serialize Protocol Buffers — the default `proto.Marshal` doesn't guarantee field order. +- **Never call `time.Now()` or any other `time` package clock function.** Accept the current time as a parameter (the workflow author gets it from `runtime.Now()`), or compute it inside a `RunInNodeMode` block. +- **Never use `math/rand` (or `crypto/rand`) directly.** Use `runtime.Rand()` from the CRE SDK, which provides a consensus-safe generator so every node produces the same sequence. +- **Never use `select` with multiple ready channels** to make a decision — see Pitfall 3 above. -Because the workflow ID is derived from these inputs, it deterministically represents a specific version of your workflow code and configuration. - -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 — simulation runs your code in the same WASM environment as production, so incompatibilities surface immediately. +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 — see [Pitfall 2](#pitfall-2-no-standard-http-client--use-the-cre-sdks-http-capability). 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 aren't 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) | See the note above. | -| `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. | +| 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, see [Pitfall 2](#pitfall-2-no-standard-http-client). 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 aren't 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) | See the note above. | +| `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. | -## Pitfall 2: No standard HTTP client — use the CRE SDK's HTTP capability +## Pitfall 2: No standard HTTP client -Do not depend on `axios`, `node-fetch`, `ws`, or any HTTP or socket library — they all sit on top of Node's `http`/`net`/`stream` modules and will not work. There is also no bare global `fetch` you can rely on directly from a library the way you would in a browser or in Node 18+. +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. There is also 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`. This isn't just a technical requirement — it's 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, even if it somehow ran. +Instead, all outbound requests must go through [`HTTPClient`](/cre/reference/sdk/http-client-ts) from `@chainlink/cre-sdk`. This isn't just a technical requirement, it's 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, even if it somehow ran. -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. Your library is a thin, well-tested wrapper around this pattern — not a replacement for it. +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. Your library is a thin, well-tested wrapper around this pattern, not a replacement for it. ```typescript -// src/index.ts — inside your library package +// src/index.ts, inside your library package import { HTTPClient, consensusIdenticalAggregation, ok, type Runtime, type NodeRuntime } from "@chainlink/cre-sdk" export interface PagerDutyOptions { @@ -13415,39 +13415,37 @@ export class PagerDuty { 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). +- 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 non-deterministic work (HTTP, secrets) 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. +- 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. ## Pitfall 3: 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) don't 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: +`Promise` and `async`/`await` exist in QuickJS, but CRE SDK capabilities (`HTTPClient`, `EVMClient`, secrets, and so on) don't 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 — 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). +- 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). ## Pitfall 4: 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. If your library does any of its own computation — not just HTTP or secrets — keep it deterministic: +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()`** inside DON-mode code — accept the current time as a parameter (the workflow author gets it from `runtime.now()`), or compute it 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. What's unsafe is randomness computed outside that runtime — for example a pre-computed value passed in, or a dependency that ships its own PRNG or native random source — since that won't go through CRE's override and will diverge across nodes. This randomness is also **not cryptographically secure** — don't use it in your library for anything security-sensitive, such as key generation or signing nonces. -- **Never use `Promise.race()`, `Promise.any()`, or `Promise.all()` with unpredictable or non-fixed ordering** around `.result()` calls. `Promise.race()`/`Promise.any()` let different nodes "win" with different sources; unpredictable `Promise.all()` usage has the same failure mode if the set or order of promises isn't fixed ahead of time. Instead, call `.result()` on each operation in a fixed, hardcoded order — 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 order you resolve them in never varies. -- **Never iterate plain `Object` keys with `for...in`** when order affects the result — object key order isn't guaranteed by spec, even though engines usually preserve insertion order. Use `Object.keys(obj).sort()` for guaranteed-deterministic order. `Map` and `Set` are safe to iterate directly since they guarantee insertion order by specification — prefer them over plain objects when your library's output depends on element order. +- **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. ## Pitfall 5: numeric precision for onchain values -If your library produces or accepts values that will end up in a smart contract call — a price, an amount, 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. +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. +Structure it as an ordinary TypeScript npm package, there's no special CRE project layout for a library, only for a workflow. ``` my-cre-library/ @@ -13483,9 +13481,9 @@ my-cre-library/ Key points: -- Declare `@chainlink/cre-sdk` as a `peerDependency`, not a regular dependency. The consuming workflow already depends on `@chainlink/cre-sdk` directly — it needs it to run at all — 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. +- 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`** (mirrors the workflow's own compiler settings so output stays compatible): @@ -13515,7 +13513,7 @@ 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, on their side. +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, on their side. ## Using your library from a CRE workflow @@ -13558,17 +13556,17 @@ export async function main() { 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. +- 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 - [ ] 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. - [ ] All outbound network calls go through `HTTPClient` (or `EVMClient` for chain reads/writes), never a third-party HTTP or socket library. -- [ ] All non-deterministic work (HTTP, secrets, time, randomness) is wrapped in `runtime.runInNodeMode()` with an explicit consensus aggregation, or delegated back to the caller's `runtime`. -- [ ] No `Promise.race()`, `Promise.any()`, or order-unpredictable `Promise.all()` around `.result()` calls — resolution order is always fixed. -- [ ] No plain-object `for...in` iteration where key order affects the output — sorted `Object.keys()`, or `Map`/`Set`, instead. -- [ ] No `async` public API wrapping SDK capability calls — expose the synchronous `.result()`-based pattern. +- [ ] 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. +- [ ] No `Promise.race()`, `Promise.any()`, or order-unpredictable `Promise.all()` around `.result()` calls; resolution order is always fixed. +- [ ] No plain-object `for...in` iteration relying on inherited properties; use `Object.keys()`, or `Map`/`Set`, instead. +- [ ] No `async` public API wrapping SDK capability calls; expose the synchronous `.result()`-based pattern. - [ ] Secrets are accepted as an ID/name, never as a literal value baked into your library. - [ ] `@chainlink/cre-sdk` is a `peerDependency`, not a bundled dependency. - [ ] Verified end-to-end with `cre workflow simulate` in a real (throwaway) CRE workflow that depends on your published or local package. From 2b99008bbcb8c1c09c4ba19c5ceb558fc5512b5c Mon Sep 17 00:00:00 2001 From: Emmanuel Jacquier Date: Mon, 17 Aug 2026 16:10:20 -0400 Subject: [PATCH 5/6] fix link --- src/config/sidebar.ts | 8 -------- src/content/cre/guides/workflow/building-a-library-go.mdx | 2 +- src/content/cre/guides/workflow/building-a-library-ts.mdx | 2 +- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/config/sidebar.ts b/src/config/sidebar.ts index 6e0f0c22ce5..99086774f4b 100644 --- a/src/config/sidebar.ts +++ b/src/config/sidebar.ts @@ -574,14 +574,6 @@ export const SIDEBAR: Partial> = { title: "Custom Rust Plugins", url: "cre/guides/operations/custom-rust-plugins-ts", }, - { - title: "Building a Reusable Library", - url: "cre/guides/operations/building-a-library", - highlightAsCurrent: [ - "cre/guides/operations/building-a-library-ts", - "cre/guides/operations/building-a-library-go", - ], - }, ], }, { diff --git a/src/content/cre/guides/workflow/building-a-library-go.mdx b/src/content/cre/guides/workflow/building-a-library-go.mdx index 44fc5a7996c..aeb8b70c3f6 100644 --- a/src/content/cre/guides/workflow/building-a-library-go.mdx +++ b/src/content/cre/guides/workflow/building-a-library-go.mdx @@ -3,7 +3,7 @@ section: cre date: Last Modified title: "Building a Reusable Library" sdkLang: "go" -pageId: "guides-operations-building-a-library" +pageId: "building-a-library" metadata: description: "How to build and publish a Go module that CRE workflows can import, including WASI/WASM restrictions and CRE SDK HTTP requirements." datePublished: "2026-08-14" diff --git a/src/content/cre/guides/workflow/building-a-library-ts.mdx b/src/content/cre/guides/workflow/building-a-library-ts.mdx index df01b3d0e51..f9a275220f7 100644 --- a/src/content/cre/guides/workflow/building-a-library-ts.mdx +++ b/src/content/cre/guides/workflow/building-a-library-ts.mdx @@ -3,7 +3,7 @@ section: cre date: Last Modified title: "Building a Reusable Library" sdkLang: "ts" -pageId: "guides-operations-building-a-library" +pageId: "building-a-library" metadata: description: "How to build and publish an npm package that CRE TypeScript workflows can import, including QuickJS/WASM restrictions and CRE SDK HTTP requirements." datePublished: "2026-08-14" From fc3214ab0a05f64d6f5608935f537fe51e862022 Mon Sep 17 00:00:00 2001 From: Emmanuel Jacquier Date: Mon, 17 Aug 2026 16:31:18 -0400 Subject: [PATCH 6/6] Removed checklist --- .../guides/workflow/building-a-library-go.mdx | 22 +++++++++---------- .../guides/workflow/building-a-library-ts.mdx | 18 +++++++-------- src/content/cre/llms-full-go.txt | 22 +++++++++---------- src/content/cre/llms-full-ts.txt | 18 +++++++-------- 4 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/content/cre/guides/workflow/building-a-library-go.mdx b/src/content/cre/guides/workflow/building-a-library-go.mdx index aeb8b70c3f6..a1221a76552 100644 --- a/src/content/cre/guides/workflow/building-a-library-go.mdx +++ b/src/content/cre/guides/workflow/building-a-library-go.mdx @@ -262,17 +262,17 @@ The workflow author still owns: ## Pre-publish checklist -- [ ] No filesystem access (`os.Open`, `os.ReadFile`, `os.WriteFile`, and similar) anywhere in your library or its dependencies. -- [ ] No direct network access (`net.Dial`, `net/http`, database drivers, gRPC dialing); all outbound calls go through `http.Client`/`http.SendRequest` (or `evm.Client` for chain reads/writes). -- [ ] HTTP calls, time, and randomness are wrapped in `cre.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. -- [ ] No goroutines, channels, or `select` across multiple channels used to make a decision that affects your function's output. -- [ ] No direct map iteration where order affects the output, use `cre.OrderedEntries`/`cre.OrderedEntriesFunc`. -- [ ] `encoding/json` v1 (not v2) and, if applicable, `proto.MarshalOptions{Deterministic: true}` for serialization. -- [ ] No `time.Now()`, `math/rand`, or `crypto/rand`, use `runtime.Now()` and `runtime.Rand()`. -- [ ] `*big.Int` for onchain integer values; a decimal type (not `float64`) for prices and other precision-sensitive values. -- [ ] Secrets are accepted as an ID/name, never as a literal value baked into your library. -- [ ] No `//go:build wasip1` tag on library files unless truly required, keep your library testable with plain `go test`. -- [ ] Verified end-to-end with `cre workflow simulate` in a real (throwaway) CRE workflow that depends on your published or local module. +- No filesystem access (`os.Open`, `os.ReadFile`, `os.WriteFile`, and similar) anywhere in your library or its dependencies. +- No direct network access (`net.Dial`, `net/http`, database drivers, gRPC dialing); all outbound calls go through `http.Client`/`http.SendRequest` (or `evm.Client` for chain reads/writes). +- HTTP calls, time, and randomness are wrapped in `cre.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. +- No goroutines, channels, or `select` across multiple channels used to make a decision that affects your function's output. +- No direct map iteration where order affects the output, use `cre.OrderedEntries`/`cre.OrderedEntriesFunc`. +- `encoding/json` v1 (not v2) and, if applicable, `proto.MarshalOptions{Deterministic: true}` for serialization. +- No `time.Now()`, `math/rand`, or `crypto/rand`, use `runtime.Now()` and `runtime.Rand()`. +- `*big.Int` for onchain integer values; a decimal type (not `float64`) for prices and other precision-sensitive values. +- Secrets are accepted as an ID/name, never as a literal value baked into your library. +- No `//go:build wasip1` tag on library files unless truly required, keep your library testable with plain `go test`. +- Verified end-to-end with `cre workflow simulate` in a real (throwaway) CRE workflow that depends on your published or local module. ## Learn more diff --git a/src/content/cre/guides/workflow/building-a-library-ts.mdx b/src/content/cre/guides/workflow/building-a-library-ts.mdx index f9a275220f7..632a1a4fe38 100644 --- a/src/content/cre/guides/workflow/building-a-library-ts.mdx +++ b/src/content/cre/guides/workflow/building-a-library-ts.mdx @@ -284,15 +284,15 @@ The workflow author still owns: ## Pre-publish checklist -- [ ] 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. -- [ ] All outbound network calls go through `HTTPClient` (or `EVMClient` for chain reads/writes), never a third-party HTTP or socket library. -- [ ] 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. -- [ ] No `Promise.race()`, `Promise.any()`, or order-unpredictable `Promise.all()` around `.result()` calls; resolution order is always fixed. -- [ ] No plain-object `for...in` iteration relying on inherited properties; use `Object.keys()`, or `Map`/`Set`, instead. -- [ ] No `async` public API wrapping SDK capability calls; expose the synchronous `.result()`-based pattern. -- [ ] Secrets are accepted as an ID/name, never as a literal value baked into your library. -- [ ] `@chainlink/cre-sdk` is a `peerDependency`, not a bundled dependency. -- [ ] Verified end-to-end with `cre workflow simulate` in a real (throwaway) CRE workflow that depends on your published or local package. +- 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. +- All outbound network calls go through `HTTPClient` (or `EVMClient` for chain reads/writes), never a third-party HTTP or socket library. +- 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. +- No `Promise.race()`, `Promise.any()`, or order-unpredictable `Promise.all()` around `.result()` calls; resolution order is always fixed. +- No plain-object `for...in` iteration relying on inherited properties; use `Object.keys()`, or `Map`/`Set`, instead. +- No `async` public API wrapping SDK capability calls; expose the synchronous `.result()`-based pattern. +- Secrets are accepted as an ID/name, never as a literal value baked into your library. +- `@chainlink/cre-sdk` is a `peerDependency`, not a bundled dependency. +- Verified end-to-end with `cre workflow simulate` in a real (throwaway) CRE workflow that depends on your published or local package. ## Learn more diff --git a/src/content/cre/llms-full-go.txt b/src/content/cre/llms-full-go.txt index beb08457966..ab17ec16b39 100644 --- a/src/content/cre/llms-full-go.txt +++ b/src/content/cre/llms-full-go.txt @@ -13337,17 +13337,17 @@ The workflow author still owns: ## Pre-publish checklist -- [ ] No filesystem access (`os.Open`, `os.ReadFile`, `os.WriteFile`, and similar) anywhere in your library or its dependencies. -- [ ] No direct network access (`net.Dial`, `net/http`, database drivers, gRPC dialing); all outbound calls go through `http.Client`/`http.SendRequest` (or `evm.Client` for chain reads/writes). -- [ ] HTTP calls, time, and randomness are wrapped in `cre.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. -- [ ] No goroutines, channels, or `select` across multiple channels used to make a decision that affects your function's output. -- [ ] No direct map iteration where order affects the output, use `cre.OrderedEntries`/`cre.OrderedEntriesFunc`. -- [ ] `encoding/json` v1 (not v2) and, if applicable, `proto.MarshalOptions{Deterministic: true}` for serialization. -- [ ] No `time.Now()`, `math/rand`, or `crypto/rand`, use `runtime.Now()` and `runtime.Rand()`. -- [ ] `*big.Int` for onchain integer values; a decimal type (not `float64`) for prices and other precision-sensitive values. -- [ ] Secrets are accepted as an ID/name, never as a literal value baked into your library. -- [ ] No `//go:build wasip1` tag on library files unless truly required, keep your library testable with plain `go test`. -- [ ] Verified end-to-end with `cre workflow simulate` in a real (throwaway) CRE workflow that depends on your published or local module. +- No filesystem access (`os.Open`, `os.ReadFile`, `os.WriteFile`, and similar) anywhere in your library or its dependencies. +- No direct network access (`net.Dial`, `net/http`, database drivers, gRPC dialing); all outbound calls go through `http.Client`/`http.SendRequest` (or `evm.Client` for chain reads/writes). +- HTTP calls, time, and randomness are wrapped in `cre.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. +- No goroutines, channels, or `select` across multiple channels used to make a decision that affects your function's output. +- No direct map iteration where order affects the output, use `cre.OrderedEntries`/`cre.OrderedEntriesFunc`. +- `encoding/json` v1 (not v2) and, if applicable, `proto.MarshalOptions{Deterministic: true}` for serialization. +- No `time.Now()`, `math/rand`, or `crypto/rand`, use `runtime.Now()` and `runtime.Rand()`. +- `*big.Int` for onchain integer values; a decimal type (not `float64`) for prices and other precision-sensitive values. +- Secrets are accepted as an ID/name, never as a literal value baked into your library. +- No `//go:build wasip1` tag on library files unless truly required, keep your library testable with plain `go test`. +- Verified end-to-end with `cre workflow simulate` in a real (throwaway) CRE workflow that depends on your published or local module. ## Learn more diff --git a/src/content/cre/llms-full-ts.txt b/src/content/cre/llms-full-ts.txt index 200f65a2ed2..cc9c0b4b90f 100644 --- a/src/content/cre/llms-full-ts.txt +++ b/src/content/cre/llms-full-ts.txt @@ -13561,15 +13561,15 @@ The workflow author still owns: ## Pre-publish checklist -- [ ] 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. -- [ ] All outbound network calls go through `HTTPClient` (or `EVMClient` for chain reads/writes), never a third-party HTTP or socket library. -- [ ] 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. -- [ ] No `Promise.race()`, `Promise.any()`, or order-unpredictable `Promise.all()` around `.result()` calls; resolution order is always fixed. -- [ ] No plain-object `for...in` iteration relying on inherited properties; use `Object.keys()`, or `Map`/`Set`, instead. -- [ ] No `async` public API wrapping SDK capability calls; expose the synchronous `.result()`-based pattern. -- [ ] Secrets are accepted as an ID/name, never as a literal value baked into your library. -- [ ] `@chainlink/cre-sdk` is a `peerDependency`, not a bundled dependency. -- [ ] Verified end-to-end with `cre workflow simulate` in a real (throwaway) CRE workflow that depends on your published or local package. +- 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. +- All outbound network calls go through `HTTPClient` (or `EVMClient` for chain reads/writes), never a third-party HTTP or socket library. +- 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. +- No `Promise.race()`, `Promise.any()`, or order-unpredictable `Promise.all()` around `.result()` calls; resolution order is always fixed. +- No plain-object `for...in` iteration relying on inherited properties; use `Object.keys()`, or `Map`/`Set`, instead. +- No `async` public API wrapping SDK capability calls; expose the synchronous `.result()`-based pattern. +- Secrets are accepted as an ID/name, never as a literal value baked into your library. +- `@chainlink/cre-sdk` is a `peerDependency`, not a bundled dependency. +- Verified end-to-end with `cre workflow simulate` in a real (throwaway) CRE workflow that depends on your published or local package. ## Learn more