Skip to main content
AI apps need a lot of API keys, database passwords, and third-party tokens. Sharing .env files is risky, and coding agents shouldn’t see your secrets. The credentials system keeps these secrets encrypted at rest in your repository as YAML files that you commit alongside your code — no external vault subscription needed. At runtime, a single encryption key decrypts everything. Each workflow can have its own scoped credentials that override shared defaults, giving you both team-wide consistency and per-workflow flexibility. You manage credentials through the CLI and read them in your steps with a simple dot-notation API.

Setting Up Credentials

Initialize credentials for your project:
output credentials init
This generates two files:
  • config/credentials.key — your 256-bit encryption key (never commit this)
  • config/credentials.yml.enc — the encrypted YAML file (safe to commit)
Add your secrets by opening the encrypted file in your editor:
output credentials edit
The CLI decrypts the file, opens it in $EDITOR, and re-encrypts when you save. Fill in your secrets:
# config/credentials.yml.enc (decrypted view)
anthropic:
  api_key: sk-ant-xxxxx
openai:
  api_key: sk-xxxxx
stripe:
  secret_key: sk_live_xxxxx
Then read them in any step:
// src/workflows/billing/steps.ts
import { step, z } from '@outputai/core';
import { credentials } from '@outputai/credentials';

export const chargeCustomer = step({
  name: 'chargeCustomer',
  inputSchema: z.object({ customerId: z.string(), amount: z.number() }),
  outputSchema: z.object({ chargeId: z.string() }),
  fn: async (input) => {
    const stripeKey = credentials.require('stripe.secret_key');
    // use stripeKey to call the Stripe API...
  }
});
credentials.require() throws a MissingCredentialError if the path doesn’t exist. Use credentials.get(path, defaultValue) when you want a safe fallback instead:
const region = credentials.get('aws.region', 'us-east-1');

Wiring LLM Keys to Environment Variables

LLM SDKs like the Anthropic and OpenAI clients read their API keys from environment variables. Rather than duplicating these keys in both your .env file and your encrypted credentials, you can wire credentials directly into environment variables using the credential: prefix. In your .env file, set the variable to credential:<dot.path> instead of the actual key:
# .env
ANTHROPIC_API_KEY=credential:anthropic.api_key
OPENAI_API_KEY=credential:openai.api_key
When the worker starts, it scans process.env for any values starting with credential: and replaces them with the decrypted values from config/credentials.yml.enc. By the time your first workflow runs, ANTHROPIC_API_KEY contains the real key — no code changes needed. This resolution is triggered by the output.hookFiles entry in your package.json:
package.json
{
  "output": {
    "hookFiles": ["node_modules/@outputai/credentials/dist/hooks.js"]
  }
}
See Error Hooks for more on the hookFiles mechanism. The worker logs confirmation on startup:
Startup hooks resolved env vars {"vars":["ANTHROPIC_API_KEY","OPENAI_API_KEY"]}
Real environment variable values are never overwritten. If ANTHROPIC_API_KEY is already set in the shell or injected by your CI/CD pipeline, the credential reference is ignored. This gives you a safe override path for production deployments without touching files.

Scoping Credentials to Environments

You likely need different API keys for development and production. The credentials system supports environment-specific files that sit alongside the global defaults:
# Initialize production credentials
output credentials init --environment production

# Initialize development credentials
output credentials init --environment development
This creates a config/credentials/ directory with per-environment files:
config/
├── credentials.yml.enc              # Global (shared defaults)
├── credentials.key                  # Global key
└── credentials/
    ├── production.yml.enc           # Production overrides
    ├── production.key               # Production key
    ├── development.yml.enc          # Development overrides
    └── development.key              # Development key
At runtime, the system checks NODE_ENV and loads the matching environment file. Only "production" and "development" are recognized — other values fall back to the global file. Edit environment-specific credentials the same way:
output credentials edit --environment production

Scoping Credentials to Workflows

Sometimes a single workflow needs its own secrets — a dedicated API key with different permissions, or a third-party token that only one workflow uses. You can scope credentials to a specific workflow:
output credentials init --workflow payment_processing
This creates credentials files inside the workflow directory:
src/workflows/payment_processing/
├── workflow.ts
├── steps.ts
├── credentials.yml.enc             # Workflow-specific secrets
└── credentials.key                 # Workflow-specific key
When a step runs inside this workflow, its credentials are deep-merged over the global ones. Workflow values win at the same path; global values fill in the rest. Global credentials:
anthropic:
  api_key: sk-ant-global
aws:
  region: us-east-1
  secret: aws-global-secret
Workflow credentials (payment_processing):
anthropic:
  api_key: sk-ant-workflow-specific
stripe:
  secret_key: sk_live_workflow
What credentials.get() sees inside the workflow:
anthropic:
  api_key: sk-ant-workflow-specific   # Overridden by workflow
aws:
  region: us-east-1                   # From global
  secret: aws-global-secret           # From global
stripe:
  secret_key: sk_live_workflow        # Added by workflow
--environment and --workflow are mutually exclusive flags. Environment scoping applies at the global level; workflow scoping applies per-workflow.

Deploying to Production

In local development, the .key files on disk handle decryption. In production, CI/CD, and Docker — where you don’t have key files — set the decryption key as an environment variable. The system resolves keys with a fallback chain:
ScopeEnvironment VariableKey File
GlobalOUTPUT_CREDENTIALS_KEYconfig/credentials.key
EnvironmentOUTPUT_CREDENTIALS_KEY_PRODUCTIONconfig/credentials/production.key
WorkflowOUTPUT_CREDENTIALS_KEY_PAYMENT_PROCESSINGsrc/workflows/payment_processing/credentials.key
The resolution order is: environment variable first, then key file. Workflow keys fall back to the global key if neither a workflow-specific env var nor a key file is found.

Docker

Pass the key as an environment variable in your Docker Compose or container config:
# docker-compose.yml
services:
  worker:
    image: your-worker-image
    environment:
      - OUTPUT_CREDENTIALS_KEY=${OUTPUT_CREDENTIALS_KEY}
      - NODE_ENV=production

CI/CD

Store the key in your CI provider’s secrets and expose it as an environment variable:
# GitHub Actions example
jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      OUTPUT_CREDENTIALS_KEY: ${{ secrets.OUTPUT_CREDENTIALS_KEY }}
      OUTPUT_CREDENTIALS_KEY_PRODUCTION: ${{ secrets.OUTPUT_CREDENTIALS_KEY_PRODUCTION }}
    steps:
      - run: output workflow run my_workflow

Key Management

Never commit key files

Add this to your .gitignore:
# Credential encryption keys
*.key
The encrypted .yml.enc files are safe to commit — they can’t be decrypted without the key.

Sharing keys with your team

Key files are created with 0o600 permissions (owner read/write only). Share keys with teammates through a secure channel — a password manager, encrypted messaging, or your organization’s secret sharing tool. Each developer needs the key file at the expected path, or the matching environment variable set.

Rotating keys

To rotate a key:
  1. Decrypt your current credentials: output credentials show > /tmp/creds.yml
  2. Re-initialize with --force to generate a new key: output credentials init --force
  3. Edit and paste your secrets back: output credentials edit
  4. Distribute the new key to your team and update environment variables in production
  5. Securely delete the temporary file: rm /tmp/creds.yml

Verifying credentials

Use the CLI to check that credentials are correctly set up:
# Show all decrypted credentials
output credentials show

# Get a specific value
output credentials get anthropic.api_key

# Check a workflow's credentials
output credentials get stripe.secret_key --workflow payment_processing
For the full API reference, see the @outputai/credentials package documentation.