Orchestrations coordinate multi-step workflows — evaluate a request, wait for approval, call an external API. But that last step often needs credentials: a GitHub App private key, a Stripe secret key, an API token for a third-party service.
Before encrypted parameters, you had two options: hardcode secrets in function code (bad), or pass them through trigger parameters in plaintext (also bad). Now there's a proper answer.
How It Works
Compute steps in orchestrations now support an `encryptedParams` field. When you create or update an orchestration, include the secrets on the step that needs them:
```json { "steps": [{ "id": "provision-repo", "type": "compute", "functionId": "fn-create-github-repo", "encryptedParams": { "GITHUB_PRIVATE_KEY": { "value": "-----BEGIN RSA...", "encrypt": true }, "GITHUB_APP_ID": { "value": "12345", "encrypt": true } } }] } ```
When you set `encrypt: true`, the value is encrypted before it hits the database. The orchestration service uses AES-256-GCM with a random IV per encryption, so even identical values produce different ciphertexts.
What Happens at Each Stage
On Save
When you create or update an orchestration with encrypted params:
- The orchestration service encrypts each param value using the active secret key
- The plaintext is replaced with the encrypted blob: `{ value: "iv:ciphertext:tag", encrypted: true, keyVersion: 1 }`
- The definition is stored in PostgreSQL with encrypted values
On Read
When you fetch an orchestration via the API, encrypted values are masked:
```json { "GITHUB_PRIVATE_KEY": { "value": "********", "encrypted": true } } ```
The API never returns encrypted blobs or plaintext. You can see which params exist, but not their values.
On Execute
When the orchestration engine runs a compute step:
- It loads the full definition from the database (with encrypted blobs)
- Decrypts each param using the stored key version
- Merges decrypted values into the step input (encrypted params take precedence on key collisions)
- Sends the merged input to the compute function as `triggerParams`
The function receives plaintext secrets in `triggerParams` — identical to how it would receive them from a regular trigger. The function code doesn't need to know whether it's running in an orchestration or from a direct trigger invocation.
Key Rotation
The `SECRET_KEYS` configuration supports versioned keys:
```json { "currentKeyVersion": 2, "keys": { "1": "old-key-still-valid-for-decryption", "2": "new-key-used-for-new-encryptions" } } ```
Each encrypted param stores its `keyVersion`, so old values remain decryptable even after rotation. New encryptions use the current key version. This means you can rotate keys without re-encrypting existing orchestrations.
Why Step-Level, Not Orchestration-Level
Secrets are scoped to the step that needs them, not the entire orchestration. This matters because:
- Least privilege: the approval step doesn't see the GitHub credentials that only the provisioning step needs
- One place to update: when credentials rotate, you update one step, not every function that uses them
- Audit clarity: you can see exactly which step holds which secrets
SDK and MCP Support
Both the SDK and MCP tools support encrypted params:
SDK: ```typescript await client.createOrchestration({ name: 'Provision Repo', steps: [{ type: 'compute', functionId: 'fn-create-repo', encryptedParams: { API_KEY: { value: 'sk_live_...', encrypt: true } } }] }); ```
MCP: The `create_orchestration` and `update_orchestration` tools accept the same format. Existing encrypted params with `encrypted: true` are preserved as-is during updates; omitted params are removed.
Encrypted parameters are available now for all orchestration compute steps.