Specification: Templating Semantics¶
Spec ID:
tower-fleet/template/v1Status: Normative Version: 1.0.0 Created: 2025-12-18
Abstract¶
This specification defines the templating semantics for variable interpolation in tower-fleet intent definitions. It establishes deterministic resolution rules, fail-fast behavior, and safe-by-default escaping.
Conformance¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
1. Goals¶
- Deterministic interpolation - Same inputs produce identical outputs
- Fail-fast on ambiguity - Missing variables are hard errors, not silent defaults
- No implicit execution - Templates are pure substitution, no code evaluation
- Safe by default - Shell contexts use escaped interpolation
2. Variable Scopes¶
Templates MAY reference variables from exactly four scopes:
| Scope | Prefix | Description | Available When |
|---|---|---|---|
| Parameters | params.<name> |
Resolved, typed intent parameters | After param resolution |
| Context | context.<name> |
Captured execution context | After context capture |
| Step Outputs | steps.<step_id>.outputs.<name> |
Outputs from completed runbook steps | After step completes |
| Facts | facts.<name> |
Runtime state and environment facts | During/after execution |
Facts Scope¶
The facts.* scope contains runtime information populated by the executor:
| Fact | Description | Available When |
|---|---|---|
facts.failed_step |
Name of step that failed | On failure |
facts.rollback_status |
Status of rollback operation | After rollback |
facts.current_step |
Currently executing step name | During execution |
facts.execution_id |
Unique execution identifier | Always |
Custom facts MAY be added via environment fact collection if configured.
Templates MUST NOT reference:
- Environment variables directly ($ENV_VAR or ${ENV.VAR})
- Arbitrary expressions or computed values
- Variables from incomplete or future steps
3. Syntax¶
3.1 Delimiters¶
Template expressions MUST use the delimiter syntax: ${...}
Valid: ${params.app}
Valid: ${context.git_sha}
Invalid: $params.app
Invalid: {{params.app}}
Invalid: $(params.app)
3.2 Identifier Rules¶
Inside ${...}, only dotted identifiers are permitted:
Examples:
Valid: ${params.app}
Valid: ${params.app_name}
Valid: ${context.git_sha}
Valid: ${steps.build_image.outputs.image_tag}
Invalid: ${params.app-name} # hyphen not allowed in identifier
Invalid: ${params['app']} # bracket notation not allowed
Invalid: ${params.app.toUpper()} # function calls not allowed
3.3 No Functions¶
In v1, template expressions MUST NOT contain:
- Function calls (${params.app | lower}, ${lower(params.app)})
- Conditionals (${params.app ? 'yes' : 'no'})
- Arithmetic (${params.count + 1})
- String concatenation operators (${params.a + params.b})
Concatenation is achieved by adjacent interpolation:
Valid: registry.internal/${params.app}:${context.git_sha}
Invalid: ${params.registry + '/' + params.app}
3.4 No Nesting¶
Template expressions MUST NOT be nested:
4. Resolution Rules¶
4.1 Resolution Order¶
Template resolution is:
1. Single-pass - Each ${...} is resolved exactly once
2. Left-to-right - Expressions are resolved in order of appearance
3. Eager - All expressions in a field are resolved before use
4.2 Missing Variable Handling¶
If a referenced variable does not exist in its scope:
- Resolution MUST fail with error type
template_missing_variable - The error MUST include:
- The full template expression (
${params.missing_var}) - The scope that was searched (
params) - Available variables in that scope (for debugging)
- Execution MUST NOT proceed
{
"error": "template_missing_variable",
"expression": "${params.environment}",
"scope": "params",
"available": ["app", "skip_build_check"],
"message": "Variable 'environment' not found in params scope"
}
4.3 Null and Empty Values¶
nullvalues MUST be treated as missing (triggertemplate_missing_variable)- Empty string
""is a valid value and MUST interpolate as empty string - Boolean
falseMUST interpolate as the string"false" - Numeric
0MUST interpolate as the string"0"
4.4 No Implicit Defaults¶
Default values MUST be applied during parameter resolution, BEFORE template rendering.
Templates MUST NOT support default syntax like ${params.app:-default}.
5. Render Modes¶
Every field containing templates MUST have an associated render mode, either explicit or inherited from field type.
5.1 Literal Mode¶
Identifier: render_mode: literal
Substitution is performed without escaping. The interpolated value is inserted as-is.
Use for: - Log messages - URLs - Resource identifiers - Display strings
Example:
# render_mode: literal (implicit for outputs)
outputs:
success:
- "Deployed ${params.app} to ${params.environment}"
Result: Deployed money-tracker to production
5.2 Shell Mode¶
Identifier: render_mode: shell
Substitution is shell-escaped to ensure the value is treated as a single token, preventing injection.
Use for:
- exec.command fields
- check.command fields
- rollback.command fields
Escaping strategy:
Values MUST be escaped using single-quote wrapping with embedded single-quote handling:
Implementation (bash):
Example:
If params.app = "my-app" and context.git_sha = "abc123":
5.3 Field Mode Defaults¶
| Field Path | Default Mode |
|---|---|
exec.command |
shell |
check.command |
shell |
rollback.command |
shell |
check.path |
literal |
check.url |
literal |
controls.lock |
literal |
resources[*].name |
literal |
resources[*].namespace |
literal |
resources[*].path |
literal |
outputs.* |
literal |
6. Allowed Template Fields¶
To maintain parsing determinism, only these fields MAY contain template expressions in v1:
6.1 Allowed¶
spec:
controls:
lock: "${params.app}-${params.environment}" # allowed
resources:
- name: "${params.app}" # allowed
namespace: "${params.app}" # allowed
path: "registry.internal/${params.app}" # allowed
prerequisites:
- check:
path: "/root/projects/${params.app}/Dockerfile" # allowed
command: "npm run build" # allowed (with cwd)
runbook:
- exec:
command: "docker build -t ${steps.build.outputs.tag} ." # allowed
rollback:
command: "docker rmi ${steps.build.outputs.tag}" # allowed
verify:
- check:
url: "https://${params.app}.bogocat.com/health" # allowed
outputs:
success:
- "Deployed ${params.app}" # allowed
6.2 Not Allowed¶
metadata:
name: "${params.app}-deploy" # NOT allowed - must be literal
version: "${context.version}" # NOT allowed - must be literal
spec:
mode: "${params.mode}" # NOT allowed - must be literal enum
risk: "${computed.risk}" # NOT allowed - must be literal enum
parameters:
- name: "${dynamic}" # NOT allowed - param names are literal
type: "${dynamic}" # NOT allowed - types are literal
7. Error Handling¶
7.1 Error Types¶
| Error | Cause | Recovery |
|---|---|---|
template_missing_variable |
Referenced variable not in scope | Fix intent definition or provide param |
template_invalid_syntax |
Malformed ${...} expression |
Fix intent definition |
template_nested_expression |
Nested ${...} detected |
Fix intent definition |
template_invalid_identifier |
Non-compliant identifier | Fix intent definition |
template_disallowed_field |
Template in non-templatable field | Fix intent definition |
7.2 Error Response¶
On any template error:
1. Emit audit event template_error with error details
2. Abort intent execution
3. Return error to caller with actionable message
8. Implementation Notes¶
8.1 Reference Implementation (bash)¶
#!/bin/bash
# Minimal template resolver
resolve_template() {
local template="$1"
local -n params_ref=$2
local -n context_ref=$3
local result="$template"
local regex='\$\{([a-z_][a-z0-9_.]*)\}'
while [[ $result =~ $regex ]]; do
local full_match="${BASH_REMATCH[0]}"
local var_path="${BASH_REMATCH[1]}"
local scope="${var_path%%.*}"
local name="${var_path#*.}"
local value=""
case "$scope" in
params) value="${params_ref[$name]}" ;;
context) value="${context_ref[$name]}" ;;
*) echo "ERROR: Unknown scope: $scope" >&2; return 1 ;;
esac
if [[ -z "${value+x}" ]]; then
echo "ERROR: template_missing_variable: $var_path" >&2
return 1
fi
result="${result//$full_match/$value}"
done
echo "$result"
}
8.2 Validation¶
Before execution, intent definitions SHOULD be validated for: 1. All template expressions use valid syntax 2. All template expressions reference declared parameters or known context keys 3. Templates only appear in allowed fields
9. Future Considerations (v2)¶
The following MAY be considered for future versions:
- Limited function set (lower, upper, replace, default)
- Conditional expressions for outputs only
- Array/map iteration for multi-resource definitions
These MUST NOT be implemented in v1.
10. Examples¶
10.1 Complete Resolution Flow¶
Intent definition:
spec:
parameters:
- name: app
type: string
required: true
- name: environment
type: enum
values: [sandbox, production]
required: true
controls:
lock: "${params.app}-${params.environment}"
runbook:
- name: build_image
exec:
command: |
docker build \
-t registry.internal/${params.app}:${context.git_sha} \
/root/projects/${params.app}
outputs:
- name: image_tag
value: "registry.internal/${params.app}:${context.git_sha}"
- name: push_image
exec:
command: docker push ${steps.build_image.outputs.image_tag}
Inputs:
Resolution (step 1 - lock):
Resolution (step 2 - build command, shell mode):
Resolution (step 3 - push command, using step output):