A secret is a named value stored encrypted in your workspace and read only by the runtime. Tools and integrations reference it as {{secrets.NAME}}; nothing ever hands the value back to you, to the model, or to the caller. This is where every API key, bearer token, webhook signing key and carrier password your agents need belongs.

Storing one

ADMIN only. In the dashboard it is Build → Secrets; over the API it is POST /v1/secrets.

The naming rule

A name must match ^[A-Za-z_][A-Za-z0-9_]*$ — letters, digits and underscores, never starting with a digit. That is exactly the identifier shape the {{secrets.NAME}} token parses, so a name that cannot be referenced cannot be stored. A dash, a space or a dot is refused. Names are case-sensitive and unique in the workspace. SCREAMING_SNAKE_CASE is the convention everywhere in these docs; nothing enforces it. A value is 1–16,384 characters. An empty value is refused.

Write-only

A stored secret is never returned by any endpoint, to anyone, including the ADMIN who stored it. A read gives you the name and a masked tail:
value_hint is the last four characters and nothing more — enough to tell two keys apart when you are rotating one, never enough to reconstruct either. Any member can read this list; only an ADMIN can write it. There is no export, no reveal button and no support path that prints a value back to you. If you have lost the original, store a new one.

Where a secret resolves, and where it does not

Writing {{secrets.NAME}} in a prompt or greeting fails validation:
That refusal is the point of the whole feature, and the next section says why.

Inside a code operation

A code operation has no template syntax, so a secret arrives as a property on the input object instead:
Only the secrets a tool actually names are put in scope for that run — the tree is scanned for {{secrets.NAME}} templates and for input.secrets.NAME property access, and the workspace’s other secrets are never sent to the sandbox.

A single token keeps its type

A string that is exactly one token resolves to the value itself, with its type intact. A token inside a larger string is substituted as text:
The header is a string with the key spliced in. The body’s token is the stored value as-is, unquoted and unmodified.

A missing name fails loudly

An empty string is never substituted for a missing credential. A {{secrets.NAME}} that resolves to nothing raises secret 'NAME' does not exist and the operation fails. It does not send an unauthenticated request and hope.
That is enforced twice:
  • At publish. A tool referencing a secret the workspace does not have is a publish error, so the mistake is caught before anything is live. The same check runs on an inline MCP server’s URL and headers.
  • At runtime. Validation cannot catch a secret that is deleted after the tool was published. That one shows up as a failed tool call mid-conversation, with secret 'NAME' does not exist. Deleting a secret is the one action here that breaks something already live, so check what references it first.

Rotating a value

Store the same name again. POST /v1/secrets is an upsert: the value is replaced, the original creator attribution is kept, and nothing that references the name has to change. Tools pick the new value up on their next run — there is no republish and no cache to clear. There is no versioning and no undo. The previous value is gone the moment the new one lands. Deleting is DELETE /v1/secrets/{secret_id}, ADMIN only. Anything still referencing the name fails at runtime from that point on.

Plaintext is promoted automatically

Integrations and carrier accounts accept a credential field in either form:
  • An existing reference, {{secrets.RESEND_API_KEY}}. The name has to exist, or the request is refused with secret 'RESEND_API_KEY' does not exist.
  • Raw plaintext. It is written into the secrets store under a sensible default name — RESEND_API_KEY for a Resend key, say — and only the reference is stored on the integration row.
If the preferred name is already taken, a suffixed variant is allocated (RESEND_API_KEY_a91c) rather than overwriting the secret that is there. An existing credential another tool depends on is never clobbered by someone connecting an integration. Either way, plaintext is never stored on the integration or carrier row. The row holds a reference; the value lives in one place, encrypted, with one way to rotate it.

Why a credential must never be a variable

The other template roots are visible to the model and can be read aloud. {{vars.name}} is substituted into the system prompt the model reads. {{userdata.field}} is session state the model reads, tools write, and the agent may repeat back to a caller. A model that can see a value can say it, put it in a summary, or pass it to a tool you did not intend. A secret is different in kind: it is resolved by the runtime, inside the operation, on the way out to your API. The model never sees it. It cannot be repeated, cannot be leaked into a transcript, and does not appear in a call’s stored request payloads. So: an API key, a bearer token, a signing secret or a password is a secret. A customer’s account number, a tenant id, a base URL for the environment this agent serves — those are variables. If a value would be damaging in a transcript, it is a secret.

Templating and data

The six roots a tool operation can read, and what each is for.

Provider keys

A different store, for a different thing: the model credentials Talqing runs your agent on.