SDK Reference: Core
This page provides a reference for the core data structures and functions of the CRE TypeScript SDK. These are the fundamental building blocks that every workflow uses, regardless of trigger types or capabilities.
Import styles
The CRE TypeScript SDK supports two equivalent import styles. Both are fully supported, produce identical behavior, and can be used interchangeably in your workflows.
Direct imports (recommended for new projects):
import { Runner, HTTPClient, EVMClient, CronCapability, handler } from "@chainlink/cre-sdk"
// Use directly
const httpClient = new HTTPClient()
const evmClient = new EVMClient(chainSelector)
Namespace imports:
import { cre, Runner } from "@chainlink/cre-sdk"
// Access through cre namespace
const httpClient = new cre.capabilities.HTTPClient()
const evmClient = new cre.capabilities.EVMClient(chainSelector)
When to use each style:
| Style | Best for |
|---|---|
| Direct imports | New projects, cleaner import statements, better tree-shaking |
| Namespace imports | Existing codebases already using this pattern, preference for grouped access |
Key concepts and components
handler()
The handler() function is the cornerstone of every workflow. It registers a handler that links a specific trigger to a callback function containing your workflow logic. It is typically called within your initWorkflow function.
Usage:
import { handler, type Runtime } from "@chainlink/cre-sdk"
const initWorkflow = (config: Config) => {
return [
handler(
// 1. A configured trigger, e.g., cron.trigger(...)
// This determines WHEN the workflow runs
triggerInstance,
// 2. The callback function to execute when the trigger fires
// This is WHERE your workflow logic lives
myCallbackFunction
),
]
}
- The Trigger: An instance of a trigger capability (e.g.,
cron.trigger(...)). This defines the event that will start your workflow. See the Triggers reference for details. - The Callback: The function to be executed when the trigger fires. The signature of your callback function must match the output type of the trigger you are using.
Runtime and NodeRuntime
These TypeScript interfaces provide access to capabilities and manage the execution context of your workflow. The key difference is who is responsible for creating a single, trusted result from the work of many nodes.
-
Runtime<C>("DON Mode"): Passed to your main trigger callback, this represents the DON's (Decentralized Oracle Network) execution context. It is used for operations that are already guaranteed to be Byzantine Fault Tolerant (BFT). When you use theRuntime, you ask the network to execute something, and CRE handles the underlying complexity to ensure you get back one final, secure, and trustworthy result. Common use cases include writing transactions to a blockchain with the EVM client or accessing secrets. -
NodeRuntime<C>("Node Mode"): Represents an individual node's execution context. This is used when a BFT guarantee cannot be provided automatically (e.g., calling a third-party API). You tell each node to perform a task on its own, and each node returns its own individual answer. You are then responsible for telling the SDK how to combine them into a single, trusted result by providing a consensus and aggregation algorithm. It is used exclusively inside aruntime.runInNodeMode()block and is provided by that function—you do not receive this type directly in your handler's callback.
To learn more about how to aggregate results from NodeRuntime, see the Consensus & Aggregation reference.
Available Methods:
Both Runtime and NodeRuntime provide:
config: Access to your workflow's configurationnow(): Returns the currentDateobject. See Time in CRE for details.log(message: string): Logs a message (see Logging below)callCapability(...): Internal method for calling capabilities (used by generated code)
Runtime additionally provides:
runInNodeMode(...): Execute code on individual nodes with consensus aggregationgetSecret(...): Access to workflow secretsreport(...): Generate cryptographically signed reports. To verify received reports, useReport.parse().
Logging
Use runtime.log() to output messages from your workflow. This is the only way to produce visible logs—console.log does not work in the WASM environment.
const onCronTrigger = (runtime: Runtime<Config>): string => {
runtime.log("Workflow started")
const result = someOperation()
runtime.log(`Operation result: ${result}`)
return "done"
}
Where logs appear:
| Environment | Location |
|---|---|
| Simulation | Terminal output with [USER LOG] prefix |
| Deployed workflow | CRE UI → Workflows → select workflow → Execution tab → click an execution → Logs tab |
See Monitoring & Debugging Workflows for details on viewing logs for deployed workflows.
Important notes:
runtime.log()accepts a single string argument—use template literals to include variables- Logs are only available inside callback functions where you have access to the
runtimeobject
Understanding the .result() Pattern
All SDK capabilities in the TypeScript SDK use a two-step pattern for asynchronous operations:
Step 1: Initiate the operation
const request = httpClient.sendRequest(runtime, { url: "https://api.example.com" })
Step 2: Get the result
const response = request.result()
Common usage: These steps are often chained together for simplicity:
import { EVMClient, encodeCallMsg, LAST_FINALIZED_BLOCK_NUMBER, type Runtime } from "@chainlink/cre-sdk"
import { zeroAddress } from "viem"
const onCronTrigger = (runtime: Runtime<Config>): string => {
const evmClient = new EVMClient(chainSelector)
// Inline pattern: initiate and get result in one expression
const contractCall = evmClient
.callContract(runtime, {
call: encodeCallMsg({
from: zeroAddress,
to: config.contractAddress,
data: encodedCallData,
}),
blockNumber: LAST_FINALIZED_BLOCK_NUMBER,
})
.result()
return "Success"
}
Why this pattern exists
Traditional TypeScript async/await doesn't work with SDK capabilities in the WebAssembly environment where CRE workflows run. WASM execution is fundamentally synchronous—when you call a function, it runs to completion before anything else happens. The interaction between the WASM guest (your workflow) and the Go host (the CRE engine) uses simple, synchronous function calls.
The .result() pattern is a custom solution to this limitation. It simulates asynchronous behavior using a pair of synchronous calls:
- The first call (e.g.,
sendRequest()) sends your request from the TypeScript code (compiled to WASM) to the CRE host - The
.result()call blocks your WASM code and waits for the host to complete the async operation and return the response
This allows the host to handle I/O-bound tasks (like network requests) asynchronously without blocking the entire runtime, while providing a simple, blocking interface to your code inside the WASM module.
Preparing multiple operations
You can initiate multiple operations before calling .result() on any of them:
// Initiate two operations
const request1 = httpClient.sendRequest(runtime, { url: "https://api1.example.com" })
const request2 = httpClient.sendRequest(runtime, { url: "https://api2.example.com" })
// Get results as needed
const response1 = request1.result()
const response2 = request2.result()
This pattern allows you to prepare operations and then collect their results in the order you need them.
Operations that use .result()
The .result() pattern applies to all SDK capabilities that perform asynchronous work:
- HTTP requests:
httpClient.sendRequest(...).result() - EVM contract calls (read):
evmClient.callContract(...).result() - EVM contract calls (write):
evmClient.writeReport(...).result() - Secrets retrieval:
runtime.getSecret(...).result() - Node-level execution:
runtime.runInNodeMode(...)().result() - Report generation:
runtime.report(...).result()
Workflow entry points
Your workflow code requires two specific functions to serve as entry points for compilation and execution.
main()
This is the entry point of your workflow. You must define this async function to create a WASM runner and start your workflow.
Required Pattern:
import { Runner } from "@chainlink/cre-sdk"
import { z } from "zod"
// Define your config schema with Zod
const configSchema = z.object({
schedule: z.string(),
apiUrl: z.string(),
})
type Config = z.infer<typeof configSchema>
export async function main() {
// Create the runner with your config schema
const runner = await Runner.newRunner<Config>({ configSchema })
// Run your workflow initialization function
await runner.run(initWorkflow)
}
All of these patterns are valid:
-
Pattern 1: Let the SDK handle everything (recommended)
export async function main() { const runner = await Runner.newRunner<Config>() await runner.run(initWorkflow) } // No need to call main() - the SDK automatically appends: // main().catch(sendErrorResponse) -
Pattern 2: Explicit call without
.catch()- SDK adds error handlingexport async function main() { const runner = await Runner.newRunner<Config>() await runner.run(initWorkflow) } main() // SDK transforms this to: main().catch(sendErrorResponse) -
Pattern 3: Custom error handling - SDK respects your handler
import { sendErrorResponse } from "@chainlink/cre-sdk" export async function main() { const runner = await Runner.newRunner<Config>() await runner.run(initWorkflow) } // If you provide .catch(), the SDK leaves it untouched main().catch((error) => { // Your custom error handling logic // You should call sendErrorResponse to report the error sendErrorResponse(error) })
sendErrorResponse reports the error to CRE and marks the execution as failed.
- In simulation, the error appears in your terminal output.
- For deployed workflows, you'll see it in the Execution tab with a
Failurestatus and the error message in the Logs tab.
Key points:
- Must be an
asyncfunction - Must call
Runner.newRunner<Config>()with an optionalconfigSchemaparameter for validation - Must call
runner.run(initWorkflow)to execute your workflow
initWorkflow
This is the second required entry point. The CRE runner calls this function to initialize your workflow and register all its handlers.
Required Signature:
import { handler, type Runtime } from "@chainlink/cre-sdk"
function initWorkflow(config: Config): Array<HandlerEntry<Config, any, any, any>>
Parameters:
config: Your workflow's configuration object (validated against your Zod schema if provided)
Returns:
- An array of handlers created with
handler()
Example:
import { CronCapability, handler, type Runtime, type CronPayload } from "@chainlink/cre-sdk"
// Callback function executed by the handler
const onCronTrigger = (runtime: Runtime<Config>, payload: CronPayload): string => {
runtime.log("Workflow triggered!")
return "complete"
}
const initWorkflow = (config: Config) => {
const cron = new CronCapability()
return [handler(cron.trigger({ schedule: config.schedule }), onCronTrigger)]
}
runtime.runInNodeMode()
As explained in the Runtime and NodeRuntime section, this method is the bridge between the DON-level execution context (Runtime) and the individual node-level context (NodeRuntime). It allows you to execute code on individual nodes and then aggregate their results back into a single, trusted outcome.
Signature:
runtime.runInNodeMode<TArgs extends unknown[], TOutput>(
fn: (nodeRuntime: NodeRuntime<C>, ...args: TArgs) => TOutput,
consensusAggregation: ConsensusAggregation<TOutput, true>,
unwrapOptions?: UnwrapOptions<TOutput>
): (...args: TArgs) => { result: () => TOutput }
Parameters:
fn: A function that receives aNodeRuntimeand executes on each individual nodeconsensusAggregation: An aggregation function (e.g.,consensusMedianAggregation<bigint>())unwrapOptions: Optional configuration for how to deserialize the consensus result. SeeUnwrapOptionsbelow.
Returns:
A function that, when called with any additional arguments, returns an object with a .result() method.
UnwrapOptions
When runInNodeMode returns a non-primitive object type, the SDK needs to know how to deserialize the consensus result back into a typed object. The unwrapOptions parameter tells the SDK which deserialization strategy to use.
Type definition:
type UnwrapOptions<T> = { schema: SchemaValidator<T>; factory?: never } | { schema?: never; factory: () => T }
Variants:
| Variant | When to use | Example |
|---|---|---|
{ schema: validator } | Objects with a Zod or any Standard Schema-compatible validator | { schema: myZodSchema } |
{ factory: () => T } | Objects without a schema — provide a factory that returns a default instance | { factory: () => ({ price: 0n, source: "" }) } |
Note: schema and factory are mutually exclusive — provide one or the other, not both.
Example with schema (most common for complex types):
import { HTTPClient, consensusMedianAggregation, type Runtime, type NodeRuntime } from "@chainlink/cre-sdk"
import { z } from "zod"
const priceResultSchema = z.object({
price: z.bigint(),
source: z.string(),
})
type PriceResult = z.infer<typeof priceResultSchema>
const fetchPrice = (nodeRuntime: NodeRuntime<Config>): PriceResult => {
const httpClient = new HTTPClient()
// ... fetch and parse price ...
return { price: 42000n, source: "api.example.com" }
}
const onTrigger = (runtime: Runtime<Config>): string => {
const price = runtime
.runInNodeMode(fetchPrice, consensusMedianAggregation<PriceResult>(), { schema: priceResultSchema })()
.result()
runtime.log(`Price: ${price.price} from ${price.source}`)
return "done"
}
Example:
This example uses runInNodeMode to fetch data from an API on each node, and then uses the DON-level Runtime to write the aggregated result onchain.
import {
HTTPClient,
consensusMedianAggregation,
type Runtime,
type NodeRuntime,
} from "@chainlink/cre-sdk"
const fetchPrice = (nodeRuntime: NodeRuntime<Config>): bigint => {
const httpClient = new HTTPClient()
// Fetch price from API using nodeRuntime
return fetchOffchainPrice(nodeRuntime)
}
const onTrigger = (runtime: Runtime<Config>, ...): string => {
// 1. Run code on individual nodes using runInNodeMode
// The fetchPrice function receives a NodeRuntime
const price = runtime
.runInNodeMode(
fetchPrice,
consensusMedianAggregation<bigint>()
)()
.result()
// 2. Now, back in the DON context, use the top-level runtime
// to perform an action that requires consensus, like an onchain write
const tx = evmClient
.writeReport(runtime, { /* ... */ })
.result()
return "success"
}
runtime.getSecret()
Retrieves a secret from the Vault DON. Secrets are key-value pairs stored securely and made available to your workflow at runtime.
Signature:
runtime.getSecret(
req: SecretRequest | SecretRequestJson
): { result: () => Secret }
SecretRequest / SecretRequestJson
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The identifier of the secret to retrieve. |
namespace | string | No | The namespace the secret belongs to. Defaults to "main" if omitted. |
Secret
The object returned by .result().
| Field | Type | Description |
|---|---|---|
id | string | The secret identifier. |
namespace | string | The secret namespace. |
owner | string | The address of the secret owner. |
value | string | The secret value. |
Example
runtime.getSecret() is available directly on the Runtime object passed to your handler function. No additional setup is required in initWorkflow.
import { type Runtime } from "@chainlink/cre-sdk"
const onTrigger = (runtime: Runtime<Config>): string => {
const secret = runtime.getSecret({ id: "my-api-key" }).result()
runtime.log(`Secret owner: ${secret.owner}`)
// Use secret.value in your workflow logic
return "done"
}
runtime.getSecrets()
Retrieves multiple secrets in a single batch call. Use this instead of calling getSecret() in a loop when you need two or more secrets.
Signature:
runtime.getSecrets(
requests: Array<SecretRequest | SecretRequestJson>
): { result: () => Record<string, Secret> }
Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
requests | Array<SecretRequest | SecretRequestJson> | Yes | Array of secrets to fetch. Duplicate IDs are not allowed. |
Each entry in requests follows the same SecretRequest shape as getSecret().
Returns:
A Record<string, Secret> (after calling .result()) where each key is the secret's id and the value is the corresponding Secret object.
Error handling:
If any secret in the batch fails, getSecrets() throws a SecretsBatchError containing all error messages aggregated from the failed secrets.
Example:
import { type Runtime } from "@chainlink/cre-sdk"
const onTrigger = (runtime: Runtime<Config>): string => {
const secretsToFetch = [{ id: "api-key" }, { id: "webhook-url" }]
const secrets = runtime.getSecrets(secretsToFetch).result()
const apiKey = secrets["api-key"].value
const webhookUrl = secrets["webhook-url"].value
runtime.log(`Fetched ${Object.keys(secrets).length} secrets`)
return "done"
}
Report verification
Parse and verify CRE reports received over HTTP or other offchain channels. Requires @chainlink/cre-sdk v1.8.0 or later.
Verification runs in your callback: signatures are checked offchain, while authorized signers are loaded from the onchain Capability Registry (default: productionEnvironment() on Ethereum Mainnet). This is not the workflow deployment registry (private or onchain).
Import from @chainlink/cre-sdk: Report, productionEnvironment, zoneFromEnvironment, and ReportParseConfig.
Report.parse()
Signature:
Report.parse(
runtime: Runtime,
rawReport: Uint8Array,
signatures: Uint8Array[],
reportContext: Uint8Array,
config?: ReportParseConfig,
): Promise<Report>
Parses a report and verifies signatures. Throws on failure. Registry reads are cached per chain and DON.
Report accessors
After a successful parse:
| Method | Returns | Description |
|---|---|---|
workflowId() | string | Workflow hash (hex) |
workflowOwner() | string | Workflow owner (hex) |
workflowName() | string | Workflow name from metadata |
executionId() | string | Execution identifier (hex) |
donId() | number | DON that produced the report |
body() | Uint8Array | Payload after the 109-byte metadata header |
seqNr() | bigint | Sequence number |
configDigest() | Uint8Array | Config digest |
reportContext() | Uint8Array | Full report context bytes |
rawReport() | Uint8Array | Full raw report bytes |
ReportParseConfig
| Field | Type | Description |
|---|---|---|
acceptedEnvironments | Environment[] | Registry environments to try (defaults to production) |
acceptedZones | Zone[] | Restrict verification to specific DON IDs |
skipSignatureVerification | boolean | Parse metadata only; not for production without another trust path |
productionEnvironment() and zoneFromEnvironment()
function productionEnvironment(): Environment
function zoneFromEnvironment(environment: Environment, donID: number): Zone
Environment field | Type | Description |
|---|---|---|
chainSelector | bigint | Chain selector for EVM reads (default: Ethereum Mainnet) |
registryAddress | string | Capability Registry contract address |