---
question: Does a workspace secret fill config.yaml placeholders?
kind: task
via_web: available
via_cli: available
via_python: available
via_api: available
via_config: available
ref_web: System › Data › Secrets
ref_cli: abi config validate
ref_python: secret.set
ref_api: PUT /api/admin/system/resources/secret/entry
ref_config: .env
prereq: A project made by `abi new project`, with its `.env` file.
next:
  - /capabilities/build/secrets/use-a-secret-in-config
  - /capabilities/build/secrets/rotate-a-secret
---

# Add a secret

An engine secret is a name and a value in the secret adapters: `.env` by default. Adding one writes it to every adapter that accepts writes. `config.yaml` reads it as `{{ secret.NAME }}`, and modules as `secret.get("NAME")`.

## Web UI

Super admins only. The value is written to every secret adapter, `.env` included.

### Open the secrets view

In Nexus, open **System** in the sidebar, then the **Data** tab, then **Secrets**. Values show as dots.

### Create the secret

Choose **New secret**. Enter the name (for example `PARTNER_API_TOKEN`) and the value, then choose **Create**. Every change is audited.

![System app, Data tab, on Secrets: the New secret dialog](/abi/Screenshot_System_New_Secret.png)

*System › Data › Secrets › New secret. The value is saved as text and masked in the list.*

### Use it

Config reads secrets when the engine starts: restart it before a new `{{ secret.NAME }}` resolves. A module that calls `secret.get` in the process that saved it sees it at once.

## CLI

### Reference it first

Add the reference where the value is needed, in `config.yaml`:

```yaml title="config.yaml"
modules:
  - module: my_module
    enabled: true
    config:
      partner_api_token: "{{ secret.PARTNER_API_TOKEN }}"
```

`partner_api_token` is a field of your module's `Configuration` class. A new project already references `OPENROUTER_API_KEY` this way, in the `naas_abi_marketplace.ai.openrouter` entry: edit that entry rather than adding a second one.

### Let the CLI ask for it

```bash
abi config validate
```

In a terminal, each secret the config references but no adapter has is asked for (`Secret 'PARTNER_API_TOKEN' not found. Please enter the value for PARTNER_API_TOKEN`), then saved to every adapter: the line lands in `.env`. The prompt shows what you type. Without a terminal, as in CI, it fails instead.

### Or push a file to Naas

For a project that keeps its secrets in Naas (the `naas` and `base64` adapters), the stored secrets are one file. Push replaces all of it, so start from what is stored:

```bash
abi secrets naas get-base64-env --naas-secret-name abi_secrets > .env.prod
```

Add the line to `.env.prod`, then push the whole file back:

```bash
abi secrets naas push-env-as-base64 --env-file .env.prod --naas-secret-name abi_secrets
```

A `.env.prod` that holds only the new line deletes every other secret. Both commands print every value: run them in your own terminal, never where the output is logged, as in CI. Multi-line values are the only ones quoted on the way out, so check values that contain ` #` or start or end with a space before you push. `--naas-api-key` defaults to `$NAAS_API_KEY`. Delete `.env.prod` afterwards. See [`abi secrets naas`](/user-guide/reference/cli#abi-secrets-naas).

## Python

### Declare the service

```python title="src/my_module/__init__.py"
from naas_abi_core.module.Module import BaseModule, ModuleDependencies
from naas_abi_core.services.secret.Secret import Secret


class ABIModule(BaseModule):
    dependencies: ModuleDependencies = ModuleDependencies(
        modules=[],
        services=[Secret],
    )
```

### Set and read it

```python
secret = self.engine.services.secret
secret.set("PARTNER_API_TOKEN", token)

value = secret.get("PARTNER_API_TOKEN")           # None when missing
value = secret.get("PARTNER_API_TOKEN", "")       # or a default
```

`set` writes every adapter, and publishes `SecretSet` with the name only. `get` returns the first adapter that has the key. `list()` returns every name and value, merged.

## REST API

The System routes of the Nexus API. Auth: a Nexus session of a super admin (`POST /api/auth/login` returns the access token). Every change is audited.

### Write the secret

The body is the value, as UTF-8 text:

```bash
curl -X PUT "http://localhost:9879/api/admin/system/resources/secret/entry?id=PARTNER_API_TOKEN" \
  -H "Authorization: Bearer $NEXUS_TOKEN" \
  --data-binary 'the-value'
```

With `abi dev up`, use the `api` port that `abi dev ports` prints. When the name exists, the call answers 409: see [Rotate a secret](/user-guide/build/secrets/rotate-a-secret?via=api).

### List the names

```http
GET /api/admin/system/resources/secret/entries
```

Values stay masked. `POST /api/admin/system/resources/secret/reveal?id=PARTNER_API_TOKEN` returns one value, and is audited.

## Config

### Add the line to `.env`

```bash title=".env"
PARTNER_API_TOKEN=the-value
```

`.env` is gitignored: keep it out of the repository. A file other than `.env` is set by the `dotenv` adapter's `path`.

### Reference it

Write `{{ secret.PARTNER_API_TOKEN }}` where `config.yaml` needs the value. How it resolves is [Use a secret in config](/user-guide/build/secrets/use-a-secret-in-config).

### Restart

The engine reads `.env` when it starts. Restart it to pick up the new line.

## Workspace secrets

Nexus has a second store, per workspace: **Settings** › **Secrets**, managed by the workspace's admins and owners. Choose **Add Secret**, enter a key name, a category (API Key, Credential, Token or Other), the value and an optional description, then **Add Secret**. The key is uppercased, with spaces turned into `_`. **Import** reads lines from a `.env`, and **Export** downloads `.env.nexus`.

Those rows are encrypted in the Nexus database (`POST /api/secrets`). Nexus chat reads a provider key from them for a workspace agent. They never fill `{{ secret.NAME }}`.
