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 workflowspec.

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_namestryesName the secret will be referenced by
encrypted_secretEncryptedData | EncryptedClientDatayesThe 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

From moco-core/src/moco/core/workflow/sys_workflow/sys.secret.yaml, the system workflow behind the CLI's secret commands:

- activity:
condition: '{{operator=="upload"}}'
type: builtin.secret.upload
input_data:
secret_name: '{{secret_name}}'
encrypted_secret: '{{encrypted_secret}}'
in_global_ns: '{{in_global_ns}}'
output_name: 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_namestryesName 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

- activity:
type: builtin.secret.get
input_data:
secret_name: database_password
in_global_ns: false # optional; true reads the shared global store
expiration_seconds: 60 # optional; defaults to 60
output_name: db_password
- activity:
type: http.request
input_data:
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

- activity:
condition: '{{operator=="list"}}'
type: builtin.secret.list
input_data:
in_global_ns: '{{in_global_ns}}'
output_name: 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_namestryesName of the secret to delete
in_global_nsboolnofalseDelete from the shared global store

Output

None.

Example

- activity:
condition: '{{operator=="delete"}}'
type: builtin.secret.delete
input_data:
secret_name: '{{secret_name}}'
in_global_ns: '{{in_global_ns}}'
output_name: 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.