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:
- 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 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.