This guide explains when to use condition-based vs action-based naming for environment variables, with practical examples and decision-making frameworks.
When naming environment variables, you face a choice:
- Condition-based (describes context/state):
RUNNING_IN_AGENT_ENV,IS_PRODUCTION,IN_CI - Action-based (describes behavior/effect):
SKIP_SLOW_TESTS,ENABLE_DEBUG,USE_CACHE
There is no universal "always use X" rule - the best choice depends on the scope of impact and purpose of the variable.
Philosophy: Describe what is happening or where you are
Examples:
NODE_ENV=production
RAILS_ENV=development
CI=true
RUNNING_IN_DOCKER=true
IS_PREVIEW_ENVIRONMENT=trueCharacteristics:
- Declarative style: "I am X"
- Describes the system's state or context
- Often triggers multiple behavioral changes
- Common for platform/infrastructure concerns
- Application logic decides what to do with the condition
Code pattern:
if env::var("RUNNING_IN_AGENT_ENV").unwrap_or_default() == "true" {
// Agent has time constraints
skip_slow_tests = true;
reduce_logging = true;
disable_interactive_prompts = true;
}Philosophy: Describe what effect you want or what behavior to change
Examples:
SKIP_SLOW_TESTS=true
ENABLE_DEBUG_LOGGING=true
USE_PRODUCTION_DATABASE=true
DISABLE_TELEMETRY=true
FORCE_COLOR_OUTPUT=trueCharacteristics:
- Imperative style: "Do X"
- Describes the intended behavior directly
- Usually controls one specific behavior
- Common for feature toggles
- Clear, single-purpose intent
Code pattern:
if env::var("SKIP_SLOW_TESTS").unwrap_or_default() == "true" {
// Very clear: skip slow tests
skip_slow_tests = true;
}Use this decision tree to choose the right approach:
Does this variable affect multiple subsystems?
├─ YES → Consider Condition-Based
│ └─ Example: ENVIRONMENT=staging affects logging, DB, cache, API endpoints
│
└─ NO → Prefer Action-Based
└─ Example: SKIP_VALIDATION=true only affects validation logic
Is this a platform/infrastructure concern?
├─ YES → Condition-Based
│ └─ Example: CI=true, IS_DOCKER=true, KUBERNETES_SERVICE_HOST
│
└─ NO → Action-Based
└─ Example: ENABLE_FEATURE_X=true, USE_CACHE=true
Do multiple developers/systems need to interpret this differently?
├─ YES → Condition-Based (let each decide behavior)
│ └─ Example: NODE_ENV=production → bundler optimizes, logger reduces verbosity
│
└─ NO → Action-Based (explicit intent)
└─ Example: COMPRESS_RESPONSES=true → clear, unambiguous
When the environment itself is the concern:
# Good: Describes where the application is running
ENVIRONMENT=production
NODE_ENV=development
RAILS_ENV=test
KUBERNETES_SERVICE_HOST=10.0.0.1
CI=trueWhy condition-based?
- Multiple subsystems need to know the context
- Different components may react differently
- Standard conventions (NODE_ENV, RAILS_ENV) aid ecosystem compatibility
Example in practice:
// package.json - Multiple tools use NODE_ENV
{
"scripts": {
"start": "NODE_ENV=production node server.js",
"dev": "NODE_ENV=development nodemon server.js"
}
}
// Webpack, Babel, Express, etc. all check NODE_ENV and adjust behaviorWhen toggling specific functionality:
# Good: Describes what behavior to change
SKIP_SLOW_TESTS=true
ENABLE_DEBUG_LOGGING=true
USE_REDIS_CACHE=true
DISABLE_RATE_LIMITING=true
FORCE_SSL=trueWhy action-based?
- Single responsibility - one variable, one behavior
- Self-documenting - immediately clear what changes
- No ambiguity about intent
- Easier to test - toggle one thing at a time
Example in practice:
// Clear, focused behavior control
if env::var("SKIP_SLOW_TESTS").unwrap_or_default() == "true" {
steps.retain(|step| !step.is_slow());
}
if env::var("ENABLE_DEBUG_LOGGING").unwrap_or_default() == "true" {
logger.set_level(Level::Debug);
}Many systems use both strategies together:
# Condition (broad context)
NODE_ENV=production
# Actions (specific overrides)
ENABLE_DEBUG=true # Override: debug even in production
SKIP_MINIFICATION=true # Override: skip minification in production
USE_LOCAL_DATABASE=true # Override: use local DB in productionPattern:
- Condition sets defaults for an environment
- Actions provide fine-grained control to override defaults
Condition-based:
env:
CI: true # Condition: "we are in CI"
GITHUB_ACTIONS: true # Condition: "we are in GitHub Actions"Why? Many tools check CI=true and adjust behavior (disable colors, reduce interactivity, etc.)
Action-based:
ENABLE_NEW_PAYMENT_FLOW=true
ENABLE_EXPERIMENTAL_API=true
ENABLE_BETA_FEATURES=trueWhy? Each variable controls one specific feature - clear, testable, manageable.
❌ Poor (action-based for context):
SKIP_HOST_NETWORK_CHECK=true # Why are we skipping? Not clear!✅ Better (condition-based):
RUNNING_IN_DOCKER=true # Context is clear
# Code decides: "if in Docker, skip host network check"Our choice: Action-based (TORRUST_TD_SKIP_SLOW_TESTS)
Rationale:
- ✅ Specific behavior toggle (not platform context)
- ✅ Single responsibility (skip slow tests only)
- ✅ Clear intent (no guessing what happens)
- ✅ Reusable in multiple contexts (agent, local dev, CI)
- ✅ Testable (easy to verify behavior)
Alternative we rejected: TORRUST_TD_RUNNING_IN_AGENT_ENV
Why rejected:
- ❌ Describes context, not intent
- ❌ Code must infer: "if agent env, then what?"
- ❌ Tied to one specific context (agent)
- ❌ Less reusable (can't use for other scenarios)
-
Use SCREAMING_SNAKE_CASE
ENVIRONMENT, notenvironmentorEnvironment- Industry standard across all languages
-
Be explicit and descriptive
- ✅
ENABLE_REQUEST_LOGGING - ❌
LOG(too vague)
- ✅
-
Use prefixes for namespacing
- ✅
TORRUST_TD_SKIP_SLOW_TESTS - Prevents conflicts with system/library variables
- ✅
-
Boolean-like variables should be obvious
- ✅
ENABLE_X=true/false - ✅
SKIP_X=true/false - ✅
IS_X=true/false - ❌
X=1(unclear what 1 means)
- ✅
-
Avoid negative logic when possible
- ✅
ENABLE_CACHE=false(clear) - ❌
DISABLE_CACHE=false(double negative) - Exception: When the default is "enabled" and you want to disable
- ✅
Common prefixes for action-based variables:
ENABLE_*- Turn on a featureDISABLE_*- Turn off a featureSKIP_*- Skip an operationUSE_*- Use a specific implementation/resourceFORCE_*- Override normal behaviorALLOW_*- Permission-relatedREQUIRE_*- Requirement-related
Examples:
ENABLE_DEBUG_MODE=true
DISABLE_TELEMETRY=true
SKIP_MIGRATIONS=true
USE_MOCK_DATA=true
FORCE_HTTPS=true
ALLOW_ANONYMOUS_ACCESS=true
REQUIRE_EMAIL_VERIFICATION=trueCommon patterns for condition-based variables:
*_ENV- Environment typeIS_*- Boolean stateIN_*- Location/contextRUNNING_IN_*- Execution contextHAS_*- Capability presence
Examples:
NODE_ENV=production
IS_PRODUCTION=true
IN_KUBERNETES=true
RUNNING_IN_CONTAINER=true
HAS_GPU=true# Bad: What does this do?
MODE=fast
OPTIMIZATION=1
CONFIG_TYPE=specialFix: Be explicit about behavior:
# Good: Clear what happens
SKIP_SLOW_TESTS=true
ENABLE_OPTIMIZATIONS=true
USE_PRODUCTION_CONFIG=true# Bad: Mixing styles without reason
IS_PRODUCTION=true
SKIP_TESTS=true
debugMode=enabledFix: Choose a consistent pattern:
# Good: Consistent style
ENVIRONMENT=production
SKIP_SLOW_TESTS=true
ENABLE_DEBUG_MODE=true# Bad: Too broad for specific behavior
AGENT_MODE=true # What does this actually do?Fix: Use action-based for specific behaviors:
# Good: Clear, specific behaviors
SKIP_SLOW_TESTS=true
REDUCE_LOG_VERBOSITY=true
DISABLE_INTERACTIVE_PROMPTS=true# Bad: One variable does too much
if env::var("RUNNING_IN_AGENT").is_ok() {
skip_tests();
disable_logging();
skip_validation();
compress_output();
use_fast_mode();
disable_colors();
// ... and 10 more things
}Fix: Separate concerns or use explicit action variables:
# Good: Each behavior is controllable
SKIP_SLOW_TESTS=true
LOG_LEVEL=warn
SKIP_VALIDATION=false
COMPRESS_OUTPUT=trueAction-based variables are easier to test:
#[test]
fn it_should_skip_slow_tests_when_env_var_set() {
std::env::set_var("SKIP_SLOW_TESTS", "true");
let steps = get_verification_steps();
assert!(!steps.iter().any(|s| s.is_slow()));
}Condition-based requires testing all derived behaviors:
#[test]
fn it_should_adapt_to_agent_environment() {
std::env::set_var("RUNNING_IN_AGENT_ENV", "true");
// Must test all behaviors that change
assert!(tests_are_skipped());
assert!(logging_is_reduced());
assert!(prompts_are_disabled());
// ... many more assertions
}If you need to migrate from condition-based to action-based (or vice versa):
// Support old and new variable names
let skip_slow_tests = env::var("SKIP_SLOW_TESTS").is_ok()
|| env::var("RUNNING_IN_AGENT_ENV").is_ok();if env::var("RUNNING_IN_AGENT_ENV").is_ok() {
eprintln!("Warning: RUNNING_IN_AGENT_ENV is deprecated. Use SKIP_SLOW_TESTS=true instead.");
}Document the new variable and migration path.
After sufficient time (version bumps, etc.), remove support for old variable.
- ✅ Describing platform/infrastructure context
- ✅ Multiple subsystems need to know the context
- ✅ Following ecosystem conventions (NODE_ENV, RAILS_ENV)
- ✅ The "environment" itself is the concern
- ✅ Controlling specific behaviors or features
- ✅ Single-purpose toggles
- ✅ Clear, testable behavior changes
- ✅ Could be reused in multiple contexts
- ✅ User/developer needs explicit control
We chose action-based (TORRUST_TD_SKIP_SLOW_TESTS) because:
- It's a specific behavior control (not platform context)
- Single responsibility (one variable, one purpose)
- Reusable across contexts (agent, dev, CI)
- Clear, testable, self-documenting
"Start action-based (specific), move to condition-based only when multiple behaviors need to coordinate."
If you find yourself checking one condition to control many unrelated behaviors, that's a sign you might need multiple action-based variables instead.
- The Twelve-Factor App - Config
- Environment Variables Naming Convention
- Docker Environment Variables Best Practices
- GitHub Actions Environment Variables
- Environment Variable Prefix ADR - Project naming convention
- Copilot Agent Pre-commit Config - Practical example of this decision