Authorize

#Secrets

A secret (an API key, a provider token) is stored write-only and is never returned by any API again. Workflows and apps don't read secrets directly: they read a reference you wire into their config, and the value is injected into server-side execution only when an action runs. The model is config-wiring:

  1. Store the value under a scope.
  2. Wire a reference{{ secrets.<scope>.<NAME> }} — into config. The presence of the reference in config is the read authorisation.
  3. Read the config key from the definition. A definition never names secrets.* itself.

Authenticate with a Personal Access Token (HTTP Basic auth) — enter it once in the Authorize bar above and every Try it below uses it.

#Scopes

Every secret lives in exactly one scope, and every reference names the scope it reads from. The scope decides who shares the value and how the runtime resolves the scope_id when a reference is read:

Scope Reference Who shares the value scope_id at read time
tenant {{ secrets.tenant.NAME }} Everyone in the tenant. The tenant.
connection {{ secrets.connection.NAME }} Every user arriving through one provider account (one registered provider organisation; a company may hold several). The caller's provider organisation (implicit).
user {{ secrets.user.NAME }} One person, across every provider they arrive through. The caller (implicit).
app {{ secrets.app.NAME }} One app activation, across its providers. The current activation (implicit).
team {{ secrets.team.<TEAM_ID>.NAME }} The members of one team. The literal team id in the reference (explicit).

The implicit scopes (connection, user, app) make one wired reference resolve to a different value per caller, so the same config serves every user, provider account, or activation with its own credential and nothing leaks across them. team is always explicit because a user may belong to many teams. NAME is UPPER_SNAKE.

#1. Store the value

Store a value under a name. The default scope is tenant; pass scope (and scopeId — camelCase, unlike most fields — for connection / team / app) to target another. Storing the same name in the same scope again overwrites it — that is rotation.

POST/access/secretsStore a secret valueAPI docs ↗Try it

The value is encrypted at rest and never comes back out of the API — GET /access/secrets lists names only, and no role or grant can retrieve a stored value. Values surface only inside a running workflow or app action.

Writing a value under a shared scope (tenant, connection, team, app) needs a secret write grant for that scope (a tenant admin holds it for every scope). A user's own user-scoped value needs no grant — see Secrets set from inside an app.

#2. Wire a reference into config

Put the scoped reference into a named slot of the config the definition runs under. For a workflow, that is the tenant config's env:

PUT/workflows/{id}/configWire a secret into a workflow's envAPI docs ↗Try it

For an app, that is the app config, an integration config, or a principal config:

PUT/apps/{specId}/configWire a secret into an app's configAPI docs ↗Try it

Which config may carry which reference:

Config Written by May reference
Workflow tenant config (env) Tenant admin Any scope.
App config / integration config Tenant admin tenant, connection, app, user — never team.
company principal config Admin / granted user {{ secrets.connection.* }} only.
team principal config Admin / granted user {{ secrets.team.<this team's id>.* }} only.
user principal config The user {{ secrets.user.* }} only.

An app or integration config is shared by everyone using the activation, so a team reference is refused there (SECRET_REF_SCOPE_VIOLATION): it carries no membership check and would resolve for any caller. Wire team-scoped secrets through a team principal config instead.

A principal config may wire only references of its own scope; a tenant reference in a principal config, or a reference whose team id differs from the config's team, is rejected the same way.

A bare, scope-less {{ secrets.NAME }} never resolves — but where it is caught differs. An app, integration, or principal config rejects it on write (INVALID_SECRET_REF). A workflow's env accepts the write and the reference fails at run time instead, as a 400 on the action — so a 200 from PUT /workflows/{id}/config is not confirmation that the references in it are well-formed.

#3. Read the config key from the definition

The workflow, job, action, or app spec reads the config key — never secrets.*. A workflow action reads config.env.PROVIDER_API_KEY; an app spec reads the slot you wired, for example config.app.provider_token, and threads it into an action through the step's input:

{
  "kind": "actions",
  "input": { "token": "{{ config.app.provider_token }}" },
  "actions": [
    {
      "name": "http",
      "config": {
        "url": "https://api.example.com/v1/sync",
        "headers": { "Authorization": "Bearer {{ input.token }}" }
      }
    }
  ]
}

At run time the wired reference resolves against the caller, the value is injected into server-side execution only, and it is scrubbed from every output, log, and response.

A {{ secrets.* }} reference lives only in config. A workflow definition or app spec that contains one is rejected on save (SECRET_REF_IN_DEFINITION). A reference arriving any other way — a form value, an action result — is refused at resolve time, so nothing can escalate past what the tenant wired.

#Secrets set from inside an app

An app can let its users provide their own credentials without an admin in the loop. Two pieces compose:

  • a secretInput form field — write-only; the render shows an "is set" signal, never the value, and on submit the value may flow only into a setSecret step;
  • a setSecret step — writes the value to the scope named in its target ({ "scope", "scopeId"?, "name", "env"? }).
"widgets": {
  "connect": {
    "widgetComponent": "form",
    "config": {
      "header": "Connect your account",
      "fields": [ { "id": "key", "label": "API key", "type": "secretInput", "target": { "scope": "user", "name": "EMAIL_API_TOKEN" } } ],
      "submit": {
        "label": "Save",
        "command": [
          { "kind": "setSecret", "target": { "scope": "user", "name": "EMAIL_API_TOKEN" }, "value": "{{ outputs.form.key }}" },
          { "kind": "notify", "level": "INFO", "message": "Connected" },
          { "kind": "refresh" }
        ]
      }
    }
  }
}

A user's own user-scoped write needs no grant; a connection / team / app write needs the secret write grant for that scope. The render context's secretsSet lists the names of the secrets already set, grouped by scope (secretsSet.user, secretsSet.connection, …), so a page can gate the form:

"layout": [
  [{ "$if": "$not('EMAIL_API_TOKEN' in secretsSet.user)", "$then": "connect" }],
  ["inbox"]
]

The wired reference — "emailToken": "{{ secrets.user.EMAIL_API_TOKEN }}" in the app config — is the same for every user; each user's own value resolves when they run the action.

#Rotate, list, and delete

  • RotatePUT /access/secrets with the same name (and scope) and a new value, or POST again. Wired references pick up the new value on the next run. Sets are idempotent on the stored key (scope, scope_id, env, name) — two same-named values under different connections, teams, or users are distinct secrets, not a collision.
  • List (names only) — GET /access/secrets, optionally ?scope=….
  • DeleteDELETE /access/secrets/{name} (add ?scope=&scopeId= for a non-tenant scope; scope_id is rejected with a 400). A reference to a deleted value fails the action with a "missing secret" error — never a silent empty.
  • Revoke access without deleting — remove the reference from config; the action no longer receives the credential.

#API reference

Every secret operation, with full schemas and an interactive console, is in the API reference.