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 cre.HandlerInTee instead of cre.Handler. Your callback receives a cre.TeeRuntime instead of a cre.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 cre.HandlerInTee instead of cre.Handler, and specify which TEEs your workflow accepts with a cre.TeeConstraint:

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"
)

type Config struct {
	Schedule string `json:"schedule"`
	URL      string `json:"url"`
}

func InitWorkflow(config *Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[*Config], error) {
	workflow := cre.Workflow[*Config]{
		cre.HandlerInTee(
			cron.Trigger(&cron.Config{Schedule: config.Schedule}),
			onCronTrigger,
			cre.AnyTee{},
		),
	}

	return workflow, nil
}

func main() {
	wasm.NewRunner(cre.ParseJSON[Config]).Run(InitWorkflow)
}

cre.AnyTee{} accepts any registered TEE in any region. To restrict to a specific TEE type and region, use cre.OneOfTees{cre.Nitro{Regions: []cre.NitroRegion{cre.NitroUsWest2}}}. See the SDK reference for all options.

Step 2: Fetch a secret inside the enclave

Your callback receives a cre.TeeRuntime. Call runtime.GetSecrets() to fetch the secrets you need in a single batch call:

func onCronTrigger(config *Config, runtime cre.TeeRuntime, _ *cron.Payload) (string, error) {
	secrets, err := runtime.GetSecrets([]*cre.SecretRequest{
		{Id: "API_KEY"},
		{Id: "DB_URL"},
	}).Await()
	if err != nil {
		return "", fmt.Errorf("failed to get secrets: %w", err)
	}
	apiKey := secrets[0].Value
	dbUrl := secrets[1].Value

	// ...
}

To fetch a single secret, use runtime.GetSecret():

secret, err := runtime.GetSecret(&cre.SecretRequest{Id: "MY_API_KEY"}).Await()
if err != nil {
	return "", fmt.Errorf("failed to get secret: %w", err)
}

Step 3: Make a capability call from inside the enclave

Capabilities that support Confidential Workflows expose a TEE-flavored method. For the HTTP capability, that's SendRequestInTee, which takes your cre.TeeRuntime directly:

	req := &http.Request{
		Url:    config.URL,
		Method: "GET",
		MultiHeaders: map[string]*http.HeaderValues{
			"Authorization": {Values: []string{"Bearer " + apiKey}},
		},
	}

	resp, err := (&http.Client{}).SendRequestInTee(runtime, req).Await()
	if err != nil {
		return "", fmt.Errorf("confidential request failed: %w", err)
	}

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 cre.Runtime for operations that require Workflow DON execution—for example, generating a signed report:

	donRuntime := runtime.UsingTheDons()

	_, err = donRuntime.GenerateReport(&cre.ReportRequest{
		EncodedPayload: []byte(resp.Body),
		EncoderName:    "evm",
		SigningAlgo:    "ecdsa",
		HashingAlgo:    "keccak256",
	}).Await()
	if err != nil {
		return "", fmt.Errorf("failed to generate report: %w", err)
	}

	runtime.Logger().Info("Confidential workflow complete")
	return string(resp.Body), nil
}

See Generating Reports: Single Values for the full delivery flow (encoding, WriteReport, etc.). For a complete example that crosses to the DON runtime and delivers a report onchain with evmClient.writeReport(), see the AI Smart Contract Audit Firewall template (TypeScript; the same pattern applies in Go).

Step 5: Simulate

Run the simulation:

cre workflow simulate my-workflow --target staging-settings

Best practices

Complete example

package main

import (
	"fmt"
	"log/slog"

	"github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http"
	"github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron"
	"github.com/smartcontractkit/cre-sdk-go/cre"
	"github.com/smartcontractkit/cre-sdk-go/cre/wasm"
)

type Config struct {
	Schedule string `json:"schedule"`
	URL      string `json:"url"`
}

func InitWorkflow(config *Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[*Config], error) {
	workflow := cre.Workflow[*Config]{
		cre.HandlerInTee(
			cron.Trigger(&cron.Config{Schedule: config.Schedule}),
			onCronTrigger,
			cre.AnyTee{},
		),
	}

	return workflow, nil
}

func onCronTrigger(config *Config, runtime cre.TeeRuntime, _ *cron.Payload) (string, error) {
	// 1. Fetch secrets dynamically, inside the enclave.
	secrets, err := runtime.GetSecrets([]*cre.SecretRequest{
		{Id: "API_KEY"},
		{Id: "DB_URL"},
	}).Await()
	if err != nil {
		return "", fmt.Errorf("failed to get secrets: %w", err)
	}
	apiKey := secrets[0].Value

	// 2. Make an HTTP request from inside the enclave.
	req := &http.Request{
		Url:    config.URL,
		Method: "GET",
		MultiHeaders: map[string]*http.HeaderValues{
			"Authorization": {Values: []string{"Bearer " + apiKey}},
		},
	}
	resp, err := (&http.Client{}).SendRequestInTee(runtime, req).Await()
	if err != nil {
		return "", fmt.Errorf("confidential request failed: %w", err)
	}

	// 3. Cross back to the DON for anything that needs consensus.
	donRuntime := runtime.UsingTheDons()
	_, err = donRuntime.GenerateReport(&cre.ReportRequest{
		EncodedPayload: []byte(resp.Body),
		EncoderName:    "evm",
		SigningAlgo:    "ecdsa",
		HashingAlgo:    "keccak256",
	}).Await()
	if err != nil {
		return "", fmt.Errorf("failed to generate report: %w", err)
	}

	runtime.Logger().Info("Confidential workflow complete")
	return string(resp.Body), nil
}

func main() {
	wasm.NewRunner(cre.ParseJSON[Config]).Run(InitWorkflow)
}

API reference

For the full list of types and methods available on cre.TeeRuntime and cre.HandlerInTee, see the Confidential Workflows Client SDK Reference.

Get the latest Chainlink content straight to your inbox.