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:

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.

Get the latest Chainlink content straight to your inbox.