Using Secrets with Deployed Workflows
When your workflow is deployed, it cannot access your local .env file or environment variables. Instead, secrets must be stored in the Vault DON—a decentralized, secure secret storage system that your deployed workflows can access at runtime.
This guide explains how to manage secrets for deployed workflows using the cre secrets CLI commands.
Prerequisites
Before managing secrets for deployed workflows, ensure you have:
- CRE CLI installed: See the Installation Guide
- Authentication: You must be logged in with
cre login - A CRE role with secret access: Listing secrets requires
cre:secret:read(every CRE role). Creating, updating, or deleting secrets requirescre:secret:write(CRE Developer or CRE Admin). See RBAC. - (Web3-keyed flow only) Owner address configured: When managing secrets owned by a workflow owner address (the onchain registry flow), your
workflow-owner-addressmust be set. Browser-auth flows on the private registry use your CRE login session and don't require this.
Auth modes: web3 key vs. browser auth
cre secrets supports two ways to authorize Vault operations, mirroring the private vs. public workflow management split used for workflow deployment:
- Web3-keyed. Each operation submits an onchain authorization tied to your linked workflow owner address (
workflow-owner-address). Use this when your workflow is deployed to the onchain registry (deployment-registry: "onchain:ethereum-mainnet"). - Browser auth (
--secrets-auth=browser). The CLI opens a browser-based OAuth (PKCE) flow against the Vault DON gateway and authorizes the operation against your CRE organization. No wallet, no gas, noworkflow-owner-addressneeded. Use this when your workflow is deployed to theprivateregistry, or when your team manages secrets at the organization level rather than per-wallet.
You can specify the mode per command with --secrets-auth. If omitted, the CLI uses the default for your target.
# Web3-keyed (onchain registry)
cre secrets create production-secrets.yaml --target production-settings
# Browser auth (private registry / org-owned)
cre secrets create production-secrets.yaml --target staging-settings --secrets-auth=browser
How secrets work with deployed workflows
The workflow is similar to local development, but with a critical difference in where secrets are stored:
- Declare: Define secret identifiers in a YAML file
- Store: Push secrets to the Vault DON using
cre secrets create - Use: Your deployed workflow accesses secrets from the Vault using
runtime.GetSecret()
Key difference from simulation:
- Local simulation: Secrets read from your environment variables or
.envfile on your machine - Deployed workflows: Secrets retrieved from Vault DON by the workflow
Secrets limits at a glance
Several different limits apply to secrets. Two of them are both 10, but they are separate limits that apply at different times:
| Limit | Value | What it limits |
|---|---|---|
| Secrets stored per owner | 100 | The total number of secrets you can have stored in the Vault for one owner (your workflow owner address, or your organization for org-owned secrets). |
| Secrets per upload request | 10 | How many secrets a single cre secrets create, update, or delete file can contain. To store more than 10 secrets, upload them in several files. |
| Secrets per batch fetch | 10 | How many secrets a single getSecrets() / GetSecrets() call can request in your workflow code. |
| Fetch calls per execution | 5 | How many times one workflow execution can fetch secrets. A single-secret call and a batch call each count as one. |
For example, you can store up to 100 secrets (ten files of 10), but a workflow can read at most 10 secrets per call and at most 5 calls per execution, so up to 50 secrets in one execution, within the size limits. See Service Quotas for the full list, including secret sizes.
Step-by-step guide
Step 1: Create a secrets YAML file
Create a YAML file at the root of your project that declares the secrets you want to store.
Example production-secrets.yaml:
secretsNames:
API_KEY:
- API_KEY_VALUE
DATABASE_URL:
- DATABASE_URL_VALUE
Structure:
secretsNames— Top-level key containing all secrets- Each secret has:
- Key (e.g.,
API_KEY) — The identifier your workflow code will use - Value — An array containing the environment variable name that holds the actual value
- Key (e.g.,
Step 2: Provide secret values as environment variables
Set the actual secret values as environment variables. These can be provided in two ways:
Option A: Export in your shell
export API_KEY_VALUE="your-actual-api-key"
export DATABASE_URL_VALUE="postgresql://user:pass@host:5432/db"
Option B: Use a .env file
Create a .env file (or add to your existing one):
# .env
API_KEY_VALUE=your-actual-api-key
DATABASE_URL_VALUE=postgresql://user:pass@host:5432/db
The cre CLI will automatically load variables from .env when you run the commands.
Step 3: Upload secrets to the Vault DON
Use the cre secrets create command to upload your secrets to the Vault:
cre secrets create production-secrets.yaml --target production-settings
What happens:
- The CLI reads your YAML file and environment variables
- It registers the request onchain (for authorization)
- It submits the secrets to the Vault DON
- The secrets are stored securely and associated with your owner address
Example output:
{"level":"info","owner":"<your-owner-address>","digest":"041eb7a8...","time":"2025-10-22T00:14:56+02:00","message":"IsRequestAllowlisted query succeeded"}
{"level":"info","digest":"041eb7a8...","deadline":"2025-10-23T22:14:56Z","time":"2025-10-22T00:14:59+02:00","message":"AllowlistRequest submitted"}
Digest allowlisted; proceeding to gateway POST
Secret created: secret_id=API_KEY, owner=<your-owner-address>, namespace=main
Secret created: secret_id=DATABASE_URL, owner=<your-owner-address>, namespace=main
Step 4: Use secrets in your workflow code
Your workflow code uses the same API to access secrets, whether running in local simulation or deployed to a workflow DON. The CRE runtime automatically retrieves secrets from the appropriate source.
| 1 | import { type Runtime } from "@chainlink/cre-sdk" |
| 2 | |
| 3 | const onCronTrigger = (runtime: Runtime<Config>): string => { |
| 4 | // Fetch the secret from the Vault DON (uses default "main" namespace) |
| 5 | const secret = runtime.getSecret({ id: "API_KEY" }).result() |
| 6 | const apiKey = secret.value |
| 7 | |
| 8 | runtime.log(`Using API key: ${apiKey.substring(0, 4)}...`) |
| 9 | |
| 10 | // Use the secret in your workflow logic |
| 11 | // ... |
| 12 | |
| 13 | return "Success" |
| 14 | } |
Important:
- The secret identifier (
"API_KEY") must match what you declared in your YAML file - Secrets are fetched at runtime from the Vault DON
- The namespace parameter is optional—defaults to
"main"if omitted - The same code works for both simulation (reads from
.env) and production (reads from Vault)
Step 5: Verify secrets are stored
You can list all secrets stored in the Vault for your owner address:
cre secrets list --target production-settings
Example output:
{"level":"info","owner":"<your-owner-address>","digest":"225d8b6f...","time":"2025-10-22T19:10:12-05:00","message":"IsRequestAllowlisted query succeeded"}
{"level":"info","digest":"225d8b6f...","deadline":"2025-10-25T00:10:12Z","time":"2025-10-22T19:10:16-05:00","message":"AllowlistRequest submitted"}
Digest allowlisted; proceeding to gateway POST: owner=<your-owner-address>, requestID=f9148fcb-3e4e-45bf-bbde-2124ddd577e4, digest=0x225d8b6f...
Secret identifier: secret_id=API_KEY, owner=<your-owner-address>, namespace=main
Secret identifier: secret_id=DATABASE_URL, owner=<your-owner-address>, namespace=main
Fetching multiple secrets
Use the batch API to fetch several secrets in one call. A batch call counts as a single fetch call against your service quotas.
| 1 | const secrets = runtime.getSecrets([{ id: "API_KEY" }, { id: "DATABASE_URL" }]).result() |
| 2 | |
| 3 | const apiKey = secrets["API_KEY"].value |
| 4 | const databaseUrl = secrets["DATABASE_URL"].value |
Managing secrets lifecycle
Updating secrets
To update existing secrets, use the cre secrets update command:
# Update your environment variable with the new value
export API_KEY_VALUE="new-api-key-value"
# Update the secret in the Vault
cre secrets update production-secrets.yaml --target production-settings
Example output:
{"level":"info","owner":"<your-owner-address>","digest":"10854ac2...","time":"2025-10-22T19:12:32-05:00","message":"IsRequestAllowlisted query succeeded"}
{"level":"info","digest":"10854ac2...","deadline":"2025-10-25T00:12:32Z","time":"2025-10-22T19:12:40-05:00","message":"AllowlistRequest submitted"}
Digest allowlisted; proceeding to gateway POST: owner=<your-owner-address>, requestID=7433514f-4008-46dd-822a-633732b64ec9, digest=0x10854ac2...
Secret updated: secret_id=API_KEY, owner=<your-owner-address>, namespace=main
Secret updated: secret_id=DATABASE_URL, owner=<your-owner-address>, namespace=main
Deleting secrets
To remove secrets from the Vault:
Step 1: Create a deletion YAML file (secrets-to-delete.yaml):
secretsNames:
- API_KEY
- DATABASE_URL
Step 2: Run the delete command:
cre secrets delete secrets-to-delete.yaml --target production-settings
About namespaces
When you look at CLI outputs, you'll notice secrets are organized by namespaces. A namespace is simply a way to group related secrets together.
Using with multi-sig wallets
All cre secrets commands support the --unsigned flag for multi-sig wallet operations. This generates raw transaction data instead of sending transactions directly.
For complete multi-sig setup and usage, see Using Multi-sig Wallets.
Troubleshooting
"Secret not found" error in deployed workflow
Problem: Your workflow throws a "secret not found" error when calling runtime.GetSecret().
Solution:
- Verify the secret exists:
cre secrets list --target production-settings - Check that the secret ID in your code matches exactly
- Recreate the secret if necessary:
cre secrets create ...
"Secret not found" but the secret exists in cre secrets list
Problem: cre secrets list shows the secret, but the workflow can't find it.
Solution:
cre secrets create, update, and delete always use the "main" namespace, and GetSecret / getSecret default to "main" when you don't pass one. If your workflow code requests a different namespace (for example "default"), the lookup fails with an error such as vault error for secret default/API_KEY: key does not exist. Remove the Namespace field or set it to "main".
"request batch size exceeds maximum of 10"
Problem: A batch fetch, or a cre secrets request, contains more than 10 secrets.
Solution:
Split the request into batches of at most 10 secrets. For cre secrets create, update, and delete, split the YAML file. The CLI reports this as cannot have more than 10 items in a single payload; check your secrets YAML. A workflow that fetches more than 10 secrets in one batch gets request batch size exceeds maximum of 10: VaultRequestBatchSizeLimit limited: cannot use 11, limit is 10.
One missing secret fails the whole batch
Problem: A batch fetch fails, even though most of the secrets in it exist.
Solution:
Batches are all-or-nothing. If any secret in the batch doesn't exist (or isn't in the namespace you requested), the whole call fails, and the error names the secret, for example vault error for secret main/MISSING_KEY: key does not exist. Check that every ID in the batch appears in cre secrets list.
Secrets fetch call limit exceeded
Problem: A workflow execution fails when it fetches secrets, and it makes more than five separate fetch calls.
Solution: Each execution can make up to 5 secret fetch calls, and a batch call counts as one. Combine your fetches into batch calls of up to 10 secrets each. See Fetching multiple secrets.
In a regular workflow, the sixth call fails with PerWorkflow.Secrets.CallLimit limited for workflow[...]: cannot use 6, limit is 5.
In a Confidential Workflow, the same limit currently surfaces as relay quorum unreachable: 0 signed responses, ... instead of naming the limit. If you see that error and your workflow makes more than five fetch calls in one execution, reduce the number of fetch calls first.
Secrets errors end with "Verify the secret name is correct..."
Problem: A secrets error ends with Verify the secret name is correct and that the secret has been configured for this workflow (or Verify the host response is complete...), but the secret name is fine.
Solution: Those closing hints are generic and are added to every secrets error. The text before them names the actual cause, such as a limit being exceeded or a key that doesn't exist. Read that part of the message first.
"Timeout expired" error
Problem: The CLI returns a timeout error when creating/updating secrets.
Solution: The onchain authorization has expired. Re-run the command to create a new authorization.
Different secrets for simulation vs. production
Problem: You want different secret values when simulating vs. running in production.
Solution:
- For simulation: Store values in your local
.envfile - For production: Use
cre secrets createwith different values - The secret IDs stay the same—only the values differ
Learn more
- Secrets CLI Reference — Complete CLI command documentation
- Using Secrets in Simulation — For local development
- Managing Secrets with 1Password — Best practice for secure secret management
- Using Multi-sig Wallets — For multi-sig secret operations