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:
{"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:
{"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:
{"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:
await centrali.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.