Skip to main content

Secret Activities

Four activities manage the secret store — the place API keys, connection strings and service credentials live so they never appear in a wfspec.

How secrets reach an activity​

Most of the time you will not use these activities at all. An activity that needs a credential takes the name of a secret rather than the secret itself, in a field ending _secret_key:

ActivityField
openai.chat.completionsapikey_secret_key
sql.query, sql.executeconnection_string_secret_key
email.sendpassword_secret_key
gdrive.*auth.credentials_secret_key
llama_index.*vectordb_info.connection_string_secret_key, embed_model_info.apikey_secret_key, llm_model_info.apikey_secret_key, auth.token_secret_key
claude_agent.queryapikey_secret_key, per-server headers_secret_key

The activity resolves and decrypts the secret internally, so nothing sensitive ever enters workflow context. A bare NAME resolves a secret owned by the calling user; global/NAME resolves one from the shared global store.

Use builtin.secret.get only where an activity has no such field — today that means http.request, whose encrypted_auth_token takes the encrypted blob directly.

Setup​

Secrets are stored in the database named by MOCO_DB_CONN_STR, under the reserved namespaces secret (global) or <user_id>:secret (per user).

No database means no durability

With MOCO_DB_CONN_STR unset the provider falls back to an in-process store and warns. Secrets then disappear on restart and are not shared between workers.

The state activities cannot reach those namespaces — they raise ReservedNamespaceError, including for another user's <user_id>:secret, and omit them from list_namespaces. Stored secrets are reachable only through the activities on this page, so the expiration below cannot be sidestepped by reading the row directly.

Encryption​

A secret is encrypted end to end and moco is careful about which form is where:

  1. The client encrypts the secret with the secret manager's RSA public key and uploads it.
  2. The server decrypts it with the private key and re-encrypts it with a symmetric key before writing it to the database. The stored form never expires.
  3. builtin.secret.get decrypts the stored form and re-encrypts it with a short expiry, so the copy handed to the workflow cannot be replayed later.

Plaintext exists only inside the secret manager and inside the activity that consumes the secret.

Defaults​

All four: 60 s timeout, 3 attempts.


builtin.secret.upload​

Stores a secret. The payload must already be encrypted with the secret manager's public key — in practice you upload secrets with the moco CLI or the console rather than from a workflow, and both do the encryption for you.

Input

FieldTypeRequiredDefaultDescription
secret_namestryes—Name the secret will be referenced by
encrypted_secretEncryptedData | EncryptedClientDatayes—The encrypted payload
in_global_nsboolnofalseStore in the shared global store instead of the caller's

EncryptedData​

RSA-encrypted directly with the secret manager's public key.

FieldTypeRequiredDescription
encrypted_datastryesThe encrypted payload
encrypt_key_namestryesName of the key used

EncryptedClientData​

For payloads too large for RSA: the client picks its own symmetric key, encrypts the data with it, and RSA-encrypts only the key.

FieldTypeRequiredDescription
encrypted_client_keyEncryptedDatayesThe client's key, RSA-encrypted
encrypted_client_datastryesThe payload, encrypted with that key

Output

FieldTypeDescription
resultbooltrue on success

Example

The pattern used by the system workflow behind the CLI's secret commands, which picks the operation from its operator input:

- task_name: upload-secret
skip_if: '{{ operator != "upload" }}'
activity:
type: builtin.secret.upload
input:
secret_name: '{{ secret_name }}'
encrypted_secret: '{{ encrypted_secret }}'
in_global_ns: '{{ in_global_ns }}'
output:
as: result

builtin.secret.get​

Returns a secret still encrypted, in a form the consuming activity can decrypt. Pass the result straight through; do not try to read it.

Input

FieldTypeRequiredDefaultDescription
secret_namestryes—Name of the secret
in_global_nsboolnofalseRead from the shared global store
expiration_secondsintno60How long the returned copy stays decryptable. 0 or negative disables expiration

Output

An EncryptedData object, or null when no such secret exists.

Example

- task_name: get-db-password
activity:
type: builtin.secret.get
input:
secret_name: database_password
in_global_ns: false # optional; true reads the shared global store
expiration_seconds: 60 # optional; defaults to 60
output:
as: db_password
- task_name: call-partner-api
activity:
type: http.request
input:
method: GET
url: https://api.partner.example.com/v1/accounts
encrypted_auth_token: '{{ db_password }}'
Fetch late, not early

The returned copy expires after expiration_seconds (60 by default), so a copy that leaks into logs, events or workflow history cannot be replayed. Decrypting an expired copy fails with EncryptedDataExpiredError.

This matters for long-running workflows: a state machine that waits on events for minutes or hours must not fetch the secret once at startup and hold it. Re-run builtin.secret.get in each state that needs it, so every use gets a freshly minted copy.

Passing 0 or a negative value disables expiration entirely, restoring the old replayable behaviour — use it only when re-fetching genuinely isn't possible.


builtin.secret.list​

Lists the names of the secrets in a store. Values are never returned.

Input

FieldTypeRequiredDefaultDescription
in_global_nsboolnofalseList the shared global store

Output

A list of secret names.

Example

- task_name: list-secret
skip_if: '{{ operator != "list" }}'
activity:
type: builtin.secret.list
input:
in_global_ns: '{{ in_global_ns }}'
output:
as: result # -> ["MY_LLM_TOKEN", "REPORTING_DB_CONN", ...]

builtin.secret.delete​

Removes a secret. Deleting a name that does not exist is not an error.

Input

FieldTypeRequiredDefaultDescription
secret_namestryes—Name of the secret to delete
in_global_nsboolnofalseDelete from the shared global store

Output

None.

Example

- task_name: delete-secret
skip_if: '{{ operator != "delete" }}'
activity:
type: builtin.secret.delete
input:
secret_name: '{{ secret_name }}'
in_global_ns: '{{ in_global_ns }}'
output:
as: result
Deleting breaks running workflows

Any workflow whose activity resolves that *_secret_key fails on its next run. Check usage before removing a shared secret.