Making a Workflow Confidential
Some workflows need to keep sensitive inputs and the computation over them—not just a single outbound request—confidential from node operators: risk thresholds, proprietary scoring, multi-step reasoning over sensitive data. Confidential Workflows are designed to make this possible by running your handler's callback inside a secure enclave, a running instance of a Trusted Execution Environment (TEE) intended to keep that computation and data confidential from the machine's own operator while it runs.
Functionally, this means registering your handler with handlerInTee instead of handler. Your callback receives a TeeRuntime instead of a Runtime: secrets are fetched dynamically from the Vault DON rather than declared upfront, and you explicitly call runtime.usingTheDons() to reach anything that needs Workflow DON consensus.
Prerequisites
This guide assumes you have:
- Enrolled in the Confidential Workflows private beta with your Chainlink account team (see Requesting Confidential Workflows Access).
- A basic understanding of CRE. If you are new, complete the Getting Started tutorial first.
- Familiarity with secrets management in CRE.
Step-by-step example
This example shows a cron-triggered workflow that fetches a secret and makes an HTTP request from inside a TEE, then crosses back to the DON to generate a report.
Step 1: Register a TEE handler
Use handlerInTee instead of handler, and specify which TEEs your workflow accepts with a TeeConstraint:
import { CronCapability, handlerInTee, type TeeRuntime } from "@chainlink/cre-sdk"
import { z } from "zod"
const configSchema = z.object({
schedule: z.string(),
url: z.string(),
})
type Config = z.infer<typeof configSchema>
const initWorkflow = (config: Config) => {
const cron = new CronCapability()
return [handlerInTee(cron.trigger({ schedule: config.schedule }), onCronTrigger, {})]
}
{} accepts any registered TEE in any region. To restrict to a specific TEE type and region, use [{ tee: "nitro", regions: ["us-west-2"] }]. See the SDK reference for all options.
Step 2: Fetch a secret inside the enclave
Your callback receives a TeeRuntime. Call runtime.getSecrets() to fetch the secrets you need in a single batch call:
const onCronTrigger = (runtime: TeeRuntime<Config>) => {
const secrets = runtime.getSecrets([{ id: "API_KEY" }, { id: "DB_URL" }]).result()
const apiKey = secrets["API_KEY"].value
const dbUrl = secrets["DB_URL"].value
// ...
}
To fetch a single secret, use runtime.getSecret():
const secret = runtime.getSecret({ id: "MY_API_KEY" }).result()
Step 3: Make a capability call from inside the enclave
Capabilities that support Confidential Workflows expose a TeeRuntime-accepting overload of their regular method.
HTTPClient.sendRequest()
accepts your TeeRuntime directly:
const response = new HTTPClient()
.sendRequest(runtime, {
url: runtime.config.url,
method: "GET",
multiHeaders: { Authorization: { values: [`Bearer ${apiKey}`] } },
})
.result()
if (!ok(response)) {
throw new Error(`confidential request failed with status: ${response.statusCode}`)
}
The request executes from inside the enclave. Trust comes from enclave attestation rather than Workflow DON consensus.
Step 4: Cross back to the DON for anything that needs consensus
Once you have a result, call runtime.usingTheDons()
to get a regular Runtime for operations that require Workflow DON execution—for example, generating a signed report:
const donRuntime = runtime.usingTheDons()
donRuntime
.report({
encodedPayload: Buffer.from(text(response)).toString("base64"),
encoderName: "evm",
signingAlgo: "ecdsa",
hashingAlgo: "keccak256",
})
.result()
runtime.log("Confidential workflow complete")
return text(response)
See Generating Reports: Single Values for the full delivery flow (encoding, writeReport, etc.). For a complete example that encodes a payload with the hexToBase64 helper and delivers it onchain with evmClient.writeReport(donRuntime, ...), see the AI Smart Contract Audit Firewall template.
Step 5: Simulate
Run the simulation:
cre workflow simulate
Best practices
Complete example
import { CronCapability, handlerInTee, HTTPClient, ok, Runner, text, type TeeRuntime } from "@chainlink/cre-sdk"
import { z } from "zod"
const configSchema = z.object({
schedule: z.string(),
url: z.string(),
})
type Config = z.infer<typeof configSchema>
const onCronTrigger = (runtime: TeeRuntime<Config>) => {
// 1. Fetch secrets dynamically, inside the enclave.
const secrets = runtime.getSecrets([{ id: "API_KEY" }, { id: "DB_URL" }]).result()
const apiKey = secrets["API_KEY"].value
// 2. Make an HTTP request from inside the enclave.
const response = new HTTPClient()
.sendRequest(runtime, {
url: runtime.config.url,
method: "GET",
multiHeaders: { Authorization: { values: [`Bearer ${apiKey}`] } },
})
.result()
if (!ok(response)) {
throw new Error(`confidential request failed with status: ${response.statusCode}`)
}
// 3. Cross back to the DON for anything that needs consensus.
const donRuntime = runtime.usingTheDons()
donRuntime
.report({
encodedPayload: Buffer.from(text(response)).toString("base64"),
encoderName: "evm",
signingAlgo: "ecdsa",
hashingAlgo: "keccak256",
})
.result()
runtime.log("Confidential workflow complete")
return text(response)
}
const initWorkflow = (config: Config) => {
const cron = new CronCapability()
return [handlerInTee(cron.trigger({ schedule: config.schedule }), onCronTrigger, {})]
}
export async function main() {
const runner = await Runner.newRunner<Config>({ configSchema })
await runner.run(initWorkflow)
}
await main()
API reference
For the full list of types and methods available on TeeRuntime and handlerInTee, see the Confidential Workflows Client SDK Reference.