Secrets
TL;DR
- A workspace secret is a named, workspace-owned third-party credential — a team's LLM token, a customer's Stripe key, a gated model hub token — that a backend references by its ID rather than by value. The value never appears in a backend graph, the backend's history, a CLI response, a REST response, or any agent-mode JSON.
- A component author declares a parameter as a secret by adding
secret: trueto itsconfig_schemaentry. The parameter type staysString; the value a backend author later binds is the secret's ID, and the platform resolves that ID to the value at deploy time. - A backend author binds the declared parameter to a workspace secret with
ppl backend change-parameter, passing the secret's ID. The request carries the ID, never the credential itself. - Secrets are listed by metadata with
ppl secret list(or in the workspace UI, Workspace → Secrets); creating, updating, rotating, and deleting them is a workspace mutation that runs only from an interactiveppl loginsession, because that is where the value is entered. - Rotating a secret by re-using its name keeps the ID stable, so every backend bound to that ID picks up the new value on its next deploy without a graph edit.
What workspace secrets actually are
A workspace secret is a named, workspace-scoped third-party credential — the team's LLM API token, a customer's Stripe key, a gated model hub token. These are the tenant's own credentials for the external services a backend talks to, not platform infrastructure credentials. They live at the workspace boundary, separate from any backend.
The flow has three roles. A component author declares which parameters carry secrets with secret: true. A backend author binds those parameters to specific workspace secrets by ID. The platform resolves each ID to its value at deploy time. The backend graph, the backend's history, every CLI response (including ppl secret list, which returns metadata only), and every API surface carry only the ID; the value reaches a component solely at deploy time.
Workspaces own secrets
A workspace is the multi-tenant boundary: every component, backend, runtime, file, secret, and team member belongs to exactly one workspace, and tenant isolation follows from that single ownership. The CLI binds to one active workspace at a time — ppl workspace switch <workspace_id> rebinds it, ppl workspace list shows the workspaces you belong to, and ppl workspace update --add-user / --remove-user adjusts membership. Because the secret store is scoped to the workspace, switching changes which secrets you see, and a teammate in another workspace never sees this one's secrets.
The security guarantee
The guarantee is specific: a workspace secret's value is never visible in:
- the backend graph definition, including any exported, forked, or inspected form;
- the output of any
pplcommand, including agent-mode JSON; - REST API responses;
- any view that shows the backend's history;
- any activity feed.
The value reaches the running component at deploy time, when the platform resolves the bound ID. From that point on, what happens to it is the component's responsibility — the platform cannot prevent a component author from logging it or exposing it through an output stream. Components handling secrets should not log them.
The destination a server-held secret is forwarded to is pinned by the platform, not chosen by a request: a client-supplied URL cannot redirect where the value is sent.
Rotating a secret — re-setting its value under the same name — keeps the ID stable, so the backend's binding survives; what changes is the value the ID resolves to. The next deploy of any backend bound to that ID picks up the new value; existing live deployments keep their original value until they are redeployed.
Mental model — bind by ID
Workspace UI Component
┌──────────────────────┐ ┌──────────────────────────┐
│ secret hf_prod │ │ reads config.hf_token │
│ id: <secret_id> │ │ as plain String │
└──────────┬───────────┘ │ (resolved at deploy time)│
│ bind by ID └─────────────▲────────────┘
│ value at
┌──────────────────────────────┐ │ deploy time
│ Backend · vertex 3 │ │
│ parameter hf_token │ │
│ type: String │ │
│ value: <secret_id> ├─────────────────┘
└──────────────────────────────┘
Three roles, three boundaries. The workspace owns the secret and the UI for managing it. The backend author binds a parameter to a secret ID. The component reads the resolved value at deploy time, like any other configuration value. The boundaries are what keep the value out of every surface except the one place it has to reach.
Component author declares the parameter
Use this framing when designing a component that needs a credential.
A component author declares which parameters carry secrets by adding secret: true to the config_schema entry. The parameter type stays String. The secret: true flag tells the platform two things: that the parameter accepts a secret ID rather than a literal value when a backend binds it, and that the platform resolves that ID to its value at deploy time.
config_schema:
hf_token:
type: String
secret: true
description: Hugging Face token for gated model repos.
The component reads config.hf_token as a plain String at deploy time, identical in shape to any other string parameter. The component never sees the ID. From the component author's perspective, a secret is just a configured string with the secret: true flag in the manifest. The author declares which parameters are secrets; the author does not enter any secret value — values are created later, from an interactive session or the workspace UI.
A secret parameter may also be declared Maybe<String> (an optional secret, defaulting to Nothing ()) or List<String> (a set of secrets, such as the keys accepted during a rotation window). An optional secret left unbound is not passed to the component at all; a List<String> secret reaches the component as a List<String> of resolved values in bind order. Every other declared type is rejected for secret: true.
The classification is the component author's call: a parameter whose value should not appear on any surface gets secret: true; everything else stays a normal parameter. The platform enforces the flag at bind time — a parameter declared secret: true rejects literal values, and a parameter not declared secret: true rejects secret IDs.
Backend author binds by ID
Use this framing whenever a backend needs to set a secret parameter.
The bind is a normal parameter mutation — the same ppl backend change-parameter verb that sets any other parameter, with the value being the secret's ID rather than a literal:
ppl backend change-parameter <bid> --vertex <v> \
--name hf_token --type String --value "<secret_id>"
The type system validates the bind at the change-parameter call. A secret: true slot rejects literal-looking values; a non-secret slot rejects values that resolve to secret IDs. A mismatch fails at edit time, before any deploy.
The ID comes from ppl secret list (or the workspace UI). Listing returns metadata only — ID, name, description, last-updated — never a value. Creating a secret returns its ID, and that ID is what the backend author binds. Creating, updating, rotating, and deleting secrets are workspace mutations that run only from an interactive ppl login session — that is the one set of operations where the value is actually entered, and that entry belongs on a surface that does not echo it into command history or automation logs.
How rotation works without graph edits
Use this framing whenever a credential needs to change.
Rotating a secret value does not require a backend edit. Re-setting a secret under its existing name keeps the ID stable across rotations, the backend's binding to that ID stays valid, and the next deploy of any backend bound to the ID picks up the new value. Existing live deployments keep their old value until they are redeployed.
This matters at team scale. When many backends are bound to the same hub token, rotating the token is a single operation, and each backend picks up the new value on its next deploy. The binding is to an ID, so a value change is invisible to every graph that references it.
The shape of the design
The split between "workspace owns the value" and "backend references the value" is what makes the rest of the design work. Two backends in the same workspace can bind the same secret ID and receive the same value at deploy. A backend cloned into another workspace is rebound to a different secret ID, with no value travelling with the graph. A team migrating credentials moves them at the workspace boundary, and the backends bound to them keep working. The ID is the contract; the value is the workspace's to manage.
The cost is one extra concept (the workspace secret) and one extra step in the bind flow (reading the ID from ppl secret list or the UI). In exchange, the value never enters a surface it should not, rotation is one operation per credential, and the secret/non-secret classification is enforced by the platform's type system at bind time.
Where this fits
Workspace secrets are the platform's first-class mechanism for tenant-owned third-party credentials. The bind-by-ID design keeps the value off every user-visible surface while leaving the common operations simple. The component author declares which parameters are secrets, the backend author binds IDs, the workspace owner manages values, and the platform resolves IDs at deploy time. Each role has one job, and the boundary between them is what keeps the value contained.
Related
- Backends — where the secret ID is bound to a vertex parameter.
- Components — where
secret: trueis declared. - Types — the parameter-type system the bind validates against.
- Solutions — backends that need third-party credentials inside a shipped product.