Workspace Secrets

A workspace secret is a named value — an API key, a password, a signing token — stored in the workspace so that endpoint and AI tool code can use it without the value appearing in the code. An administrator stores the value once; code refers to the secret by name and reads it through one accessor, which records the access in the audit log.

Creating a secret

Secrets are administered under Manage → Configure → Secrets (#/manage/configure/secrets). The page is system-admin only, and creating, editing or deleting a secret requires a system admin with write access.

  1. Open Manage → Configure → Secrets and click Add secret.
  2. Give it a Name (up to 128 characters, no line breaks or control characters; names are unique, compared case-insensitively).
  3. Optionally add a Description (up to 512 characters) saying what the credential is for.
  4. Paste the value into Secret and click Create.

The list then shows the name, the description, whether a value is stored, and the Use in code identifier — the exact Secrets.<Name> reference to paste into an endpoint or tool, with a copy button. That identifier is derived from the name (My API key becomes Secrets.MyApiKey); when two names reduce to the same identifier, the second one is suffixed (Secrets.MyApiKey_2). The admin page and the code generator compute it the same way, so what the page shows is what compiles.

The value is write-only

Once stored, a value is never sent back to the interface or to any other caller. The list carries only the name, the description and a "has a value" flag. A secret with no value stored is flagged No value — code reading it gets an empty string.

Editing works the same way: leave the Secret field empty and only the name and description change; the stored value is left exactly as it was. So renaming a secret, or rewriting its description, never requires reading the value back, and cannot disturb it. Type a new value only when you intend to replace the old one.

The restriction is enforced in the storage layer, not only in the admin interface. A secret node serializes to nothing at all, and its value is not exposed as a field:

  • it never appears in a node preview, a search result or a query result;
  • it is not written to any text index;
  • it is not part of a workspace definitions export;
  • the generic field APIs refuse it — GetString("Value"), node["Value"] and LazyNode.GetField throw instead of returning or overwriting the value.

Reading a secret from code

One accessor reads a value:

Task<string> GetSecretAsync(SecretUID secret)

It is available on the endpoint execution scopes (CodeEndpointExecutionScope and ReadOnlyCodeEndpointExecutionScope, so read-only endpoints and replicas can use it too), on ToolScope for AI tools, and on the Shell and scheduled-code scope (ShellCodeExecutionScope), where there is no calling endpoint or tool for the audit entry to name. The argument is a SecretUID taken from the generated Secrets helper class — there is no overload that takes a name string, and holding a SecretUID grants nothing on its own: the value is resolved, and the access audited, inside the call.

In an endpoint, scope members are top-level identifiers, so the call has no receiver:

Endpoint — forward an event to an external API
var apiKey = await GetSecretAsync(Secrets.EventsApiKey);

using var http = new HttpClient();
http.DefaultRequestHeaders.Add("Authorization", "Bearer " + apiKey);

var response = await http.PostAsync(
    "https://api.example.com/v1/events",
    new StringContent(Body, Encoding.UTF8, "application/json"),
    CancellationToken);

if (!response.IsSuccessStatusCode) return StatusCode((int)response.StatusCode, "The upstream API rejected the event.");

return Ok(await response.Content.ReadAsStringAsync());

In an AI tool the same call goes through the tool's scope:

AI tool — call a shipping provider
[Tool("Look up the delivery status of an order in the shipping provider's API")]
public static async Task<string> GetDeliveryStatus(ToolScope scope,
      [Parameter("The order number", required: true)] string orderNumber)
{
    var apiKey = await scope.GetSecretAsync(Secrets.ShippingApiKey);

    using var http = scope.GetHttpClient();
    http.DefaultRequestHeaders.Add("X-Api-Key", apiKey);

    var status = await http.GetStringAsync($"https://api.example.com/orders/{orderNumber}");

    return status;
}
Use the value, don't keep it

Read the secret at the point of use and pass it straight to the call that needs it. Don't log it, don't cache it in a static field, don't put it in an endpoint response, and never write it into an AI tool's result — a tool's result goes back to the model.

The Secrets class is one of the auto-generated helpers the workspace prepends to your code, alongside N, E, Endpoints, AI_Tools and Agents. It is regenerated whenever a secret is added, renamed or deleted, and only names and UIDs are generated into it — a value is never part of the injected code. Because the reference is a compile-time identifier, deleting or renaming a secret that code still refers to breaks that code's next compile instead of failing at runtime.

Every read is audited

GetSecretAsync writes a Secret Accessed audit entry naming the user the code was running for, the secret that was read, and the endpoint or AI tool the read came from. The value itself is never part of the entry.

The caller is identified from the running endpoint's or tool's own UID (CurrentEndpointUID on the endpoint scopes, CurrentToolUID on ToolScope), so the entry reads as "…accessed secret 'Shipping API key' (<uid>) via AI tool 'Delivery status' (<uid>)". A read with no user attached — a token-authenticated endpoint call, a scheduled run — records "no user"; a read from the Shell records the user without a calling endpoint.

Entries are written when auditing is enabled for the workspace and the Secret Accessed type is turned on under Manage → Access → Audit log (#/manage/access/audit). It is part of the default set the audit page's Configure Auditing action turns on. Administrative changes to the secrets themselves — created, updated, value replaced, deleted — are recorded separately as Admin Action entries.

Secrets in a definitions export and import

Secrets take part in the workspace definitions export so that imported endpoint and AI tool code compiles, but their values never travel.

  • Export. Each secret is written to config/secrets/ as a declaration carrying its name and description. The value field is always the literal placeholder NOT EXPORTED, so anyone reading the bundle — or diffing it in the configuration-tracking repository — can see the value was withheld rather than wonder whether the export missed it.
  • Import. Secret declarations are imported first, before any code, so the Secrets helper class has every identifier the imported code references. The placeholder is never read back.
  • An existing secret is left untouched. If a secret of that name already exists in the target workspace, the import skips it entirely — importing definitions never overwrites a live credential.
  • A missing secret is created empty, and the import reports a warning naming it: the secret was created with an empty value — set it under Manage / Configure / Secrets before the code that reads it runs.

So promoting configuration from staging to production imports the shape of your secrets and leaves you to fill in the production values, once, in the target workspace.

When to use a secret, code, or an environment variable

Where the value lives Use it for Why
A workspace secret Credentials that endpoint, AI tool, task or Shell code calls out with: third-party API keys, service passwords, signing tokens, webhook secrets. The value is not in the code, so it is not in the editor, the definitions export, the configuration-tracking Git repository or a code review. It can be replaced without editing code, and every read is attributed to a user and a caller.
A literal in the code Nothing. The value follows the code everywhere the code goes: the editor, the export bundle, the Git mirror, every backup of the definitions. Rotating it means editing and redeploying code, and there is no record of who read it.
An environment variable (MSK_*) Configuration the process needs before or independently of the graph: MSK_JWT_KEY, MSK_GRAPH_MASTER_KEY, MSK_ADMIN_PASSWORD, database and proxy settings. See the configuration reference. These are read at start-up, are part of how the deployment is provisioned, and are set through your platform's own secret manager. Changing one means restarting the workspace.

A rough rule: if the value is consumed by code you write inside the workspace, it belongs in a secret. If it is consumed by the workspace process itself before your code runs, it belongs in the environment.

What a secret is not

  • Not per-endpoint access control. Any code compiled in the workspace can read any secret by referencing it. The controls around a secret are who is allowed to write endpoint and AI tool code (system admins) and the audit trail of every read — not a per-secret permission.
  • Not a substitute for your platform's secret manager. The value is stored as workspace data, and is covered by the same at-rest protection and the same backups as the rest of the graph — no more, no less. The MSK_* credentials that protect the workspace itself still belong in AWS Secrets Manager, Azure Key Vault, GCP Secret Manager or Vault, as described on the security page.
  • Not a versioned store. Replacing a value overwrites it. There is no history and no way to read the previous value back, so rotate by writing the new value and confirming the callers still work.
  • Not retrievable, ever. No admin action, export, API call, query or field read returns a stored value. If you lose the original, mint a new credential at the provider and store that.
© 2026 Curiosity. All rights reserved.