- 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 inindex.yml:
{{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.
Runtime API
For automations that need to create or rotate secrets at runtime (OAuth tokens, refresh tokens, per-user credentials), thesecrets module exposes set, get, and delete.
How it works
secrets.setandsecrets.getreturn an opaque secret reference (a$secret:...string), not the actual value.- When this reference is used inside a
fetchinstruction (in the URL, headers, body, query, or auth fields), the platform automatically resolves it to the real value just before the HTTP call. - The resolved value is redacted from all logs and events.
secrets.get right before using the reference; do not store it for later reuse.
Scopes
Access model — who can read a secret
Whethersecrets.get (and set/delete) succeeds depends on three independent things:
- The scope requested —
userorworkspace. - Whether the run carries an authenticated user — i.e.
ctx.source.userIdis 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. - Whether the call took on one of the workspace’s own roles —
assumeRoleon theruninstruction, which only ever helps aworkspacesecret (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
Aworkspace 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 — asecure_secrets rule in the workspace’s security.yml, carried by no role, so it applies to every member. Example (Figma connector):
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):
{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:- a rule carrying no
role:lands ondefault, 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.
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.
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.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 byset or get wherever a sensitive value is needed in a fetch instruction. The platform resolves it transparently:
fetch fields: url, headers, body, query, and auth (including auth.awsv4).
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: