Skip to main content
The secrets module lets automations securely store and retrieve sensitive values (API keys, tokens, credentials) at runtime. Values are encrypted at rest and never exposed as plaintext in automation outputs or logs. This page documents two things:
  • The runtime API (secrets.set, secrets.get, secrets.delete) for automations.
  • The Settings → Secrets form in the Builder UI, which is where workspace-level secrets are typically configured before any automation runs.

Settings → Secrets form

Open Settings → Secrets in the Builder. The form has two sections.

Schema-defined secrets

The top section is generated from the workspace’s secret schema, declared in index.yml:
Every property of the schema is rendered as an input field. These are the “main” secrets of the workspace: they have known names, automations reference them as {{secret.<name>}}, and someone (usually the workspace author) committed the schema to source control. Typing a value and clicking Save Secrets posts the new values to POST /workspaces/{id}/secrets. Values are stored encrypted; the form never reads back plaintext (an empty field means “leave the existing value untouched”, not “clear the value”).

Additional Secrets form

The “Additional Secrets” section underneath is for secrets that are not declared in the schema. The label “Additional” is intentional: these are extras beyond the schema-defined set. Use the Additional form when:
  • You are prototyping and have not yet committed a schema.
  • You need an environment-specific override (e.g. a per-deploy webhook URL) that should not appear in source.
  • You want a one-off value usable from a single automation without touching index.yml.
Once a name stabilizes, promote it to the schema so the next workspace clone has the documentation built in.
The “Additional Secrets” form persists plaintext values to the workspace’s encrypted secret store. It is not a temporary scratchpad; entries survive reloads and pulls (unless overwritten by a pulled index.yml).

Runtime API

For automations that need to create or rotate secrets at runtime (OAuth tokens, refresh tokens, per-user credentials), the secrets module exposes set, get, and delete.

How it works

  1. secrets.set and secrets.get return an opaque secret reference (a $secret:... string), not the actual value.
  2. When this reference is used inside a fetch instruction (in the URL, headers, body, query, or auth fields), the platform automatically resolves it to the real value just before the HTTP call.
  3. The resolved value is redacted from all logs and events.
Secret references are short-lived (a few minutes). Always call secrets.get right before using the reference; do not store it for later reuse.

Scopes

Access model — who can read a secret

Whether secrets.get (and set/delete) succeeds depends on three independent things:
  1. The scope requested — user or workspace.
  2. Whether the run carries an authenticated user — i.e. ctx.source.userId is set. Interactive runs (a user clicking in the UI, an API call with a user token, a page render) carry a user. Cron/schedule triggers, anonymous webhooks, emit-triggered runs, and in-process cross-workspace calls do not.
  3. Whether the call took on one of the workspace’s own rolesassumeRole on the run instruction, which only ever helps a workspace secret (see Reading a workspace secret with no user).

Access matrix

set and delete follow the same path as get, under the same assumed role: a run that can read a rotating token can also store its replacement, and drop one the provider has rejected for good. Reading without writing would be a half-feature — the run would burn the token and fail on the next pass — and without deleting, a dead token is retried forever.

Reading a workspace secret with no user

A workspace secret is invisible to a run that carries no user: reads resolve RBAC against the caller, and a user-less trigger holds no role, so it sees nothing. That is exactly the situation of a scheduled synchronisation — the credentials the workspace’s own members deposited are unreadable to the workspace’s own sync. Nothing about the shape of the run answers it. An unauthenticated cron and an unauthenticated HTTP call are indistinguishable, and a sync typically reaches the connector by endpoint rather than by schedule. So the workspace states the entitlement itself: it declares a role in its security.yml, and the automation takes that role on for the one call that needs it.

1. Members deposit their own credential

A connector lets its members write their own token — a secure_secrets rule in the workspace’s security.yml, carried by no role, so it applies to every member. Example (Figma connector):
The platform validates this rule strictly and forces two conditions you do not write — workspaceId (the current workspace) and scope: workspace. The rule must obey: A connector holding several credentials per remote account carries a discriminator with the fixed-width class (SharePoint):
The width must be fixed ({16}, never + / * / {n,m}) and the class must sit before ${user.id}: a variable-width segment next to the id would let two different (account, user) pairs spell the same name.

2. A role reads — and writes back — the whole family

The run that replays those tokens has no user to bind to, so its rule names a family of secrets instead, and it is carried by a role the workspace declares:
What a rule must prove depends on who it applies to, not on the action:
  • a rule carrying no role: lands on default, which applies to every member, so ${user.id} stays mandatory — it is the only thing keeping members apart;
  • a rule carried by a role applies to whoever takes it on, which is an automation acting for the workspace. It may name a family, for reads (a sync serves everyone) and for writes alike (a provider rotating its refresh tokens hands back a new one that must be stored for a member who is not there). A role-carried rule may also pin ${user.id} — strictly narrower, and accepted on its own terms.
A family pattern is checked as strictly as the user-bound one: The head is what a caller cannot vary, and it is what names the family — ending it on a separator is what stops onnRefresh_ from also matching onnRefreshOther. The resulting pattern deliberately matches every member’s figRefresh_<id>: that breadth is the point, a scheduled sync must be able to reach any connected user’s token.

3. The automation takes the role on

assumeRole applies to that single call. Everything around it keeps the caller’s own rights, and nothing is carried to the automations it calls, to an emit, or across a workspace boundary. It replaces the caller’s rights rather than adding to them, so the role has to be sufficient on its own for what the call does. Only a role the workspace declared can be assumed: owner is refused, so a developer has to state what they are opening. And whatever the role, scope: user stays out of reach — the module refuses it without a user before RBAC is consulted.
Anything holding that role can read and overwrite every credential in the family. That is the price of servicing absent users, and it is why the role should exist for that purpose alone: do not assign it to a person, and do not give it an auth block. An API key bound to it would hand every member’s credential to whoever holds the key. The platform does not prevent either — it is yours to decide.
Per-user connector tokens (OAuth refresh tokens, etc.) are not stored as user secrets. They are workspace secrets whose name embeds the user id (figRefresh_<id>), deposited under the rule above. That is why a role — which only ever reaches workspace secrets — is what lets a scheduled sync replay a given user’s token.
See RBAC → secure secrets for the security rules, and Automations → assumeRole for the instruction.

Functions

set: Store or update a secret

Returns a $secret:... reference string. If a secret with the same name and scope already exists, it is updated.

get: Retrieve a secret reference

Returns a $secret:... reference string, or { error: "not_found" } if the secret does not exist (or has expired).

delete: Remove a secret

Returns { deleted: true } on success, or { error: "not_found" }.

Using secret references in fetch

Pass the reference returned by set or get wherever a sensitive value is needed in a fetch instruction. The platform resolves it transparently:
Resolution is supported in these fetch fields: url, headers, body, query, and auth (including auth.awsv4).
Secret references expire after a few minutes. Always retrieve the reference (secrets.get) in the same automation run where you use it.

Complete example: storing and using an OAuth token

Error handling

All three functions return an error object instead of throwing when the error is actionable: Use onError or a conditions block to handle these: