| name | solution-spec |
| description | Solution YAML specification reference for scafctl. Schema, resolver phases, action workflow, ValueRef format, DAG resolution, and testing. Use when working on solution loading, parsing, execution, or spec types. |
Solution Spec Reference
Top-Level Structure
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: my-solution
version: 1.0.0
displayName: My Solution
description: Short description
category: infrastructure
tags: [go, cloud]
maintainers:
- name: Team
email: team@example.com
links:
- name: Docs
url: https://example.com
icon: https://example.com/icon.png
banner: https://example.com/banner.png
catalog:
visibility: public
beta: false
disabled: false
spec:
resolvers: {}
workflow: {}
testing: {}
Resolver Structure
Resolvers are the DAG nodes. Map keys are resolver names (DNS-safe).
spec:
resolvers:
my-resolver:
description: What this resolver does
type: string
sensitive: false
when: "_.some_flag"
dependsOn: [other]
timeout: 30s
example: "sample"
resolve:
- provider: parameter
with:
prompt: "Enter name"
default: "world"
- provider: env
with:
name: MY_ENV_VAR
until: "_ != ''"
transform:
- provider: cel
with:
expression: "_.upperAscii()"
- provider: gotmpl
with:
template: "prefix-{{.}}"
validate:
- provider: validation
with:
rules:
- expr: "size(_) > 0"
message: "Must not be empty"
messages:
error: "Failed to resolve {{.name}}"
Phase Execution Order
- Resolve: Providers execute in order. First non-null result wins (
until: controls early stop).
- Transform: Applied sequentially.
__self is the current value. Supports forEach for array iteration.
- Validate: All rules checked. Resolver fails if any validation fails.
forEach (Resolve and Transform Steps)
forEach is supported on both resolve.with and transform.with steps. It is NOT supported on validate.with.
Key difference: On resolve steps, forEach.in is required (no __self available). On transform steps, forEach.in defaults to __self.
resolve:
with:
- provider: http
forEach:
in:
rslvr: moduleList
item: mod
concurrency: 10
inputs:
url:
expr: 'mod.url'
transform:
with:
- provider: cel
forEach:
item: num
index: i
inputs:
expression: "num * 2"
Fields: item, index, in (ValueRef; required on resolve, defaults to __self on transform), concurrency (int), keepSkipped (bool), onError (actions only: fail|continue).
Context variables: __item and __index are always injected. Custom aliases (item, index fields) are added alongside them.
Dependency Resolution (DAG)
Dependencies are extracted automatically from:
- CEL expressions:
_.resolverName and _["resolverName"] references
- Resolver references:
rslvr: resolverName in ValueRef inputs
- Go templates:
{{.resolverName}} references
- Explicit
dependsOn list (merged with auto-inferred deps)
Important: dependsOn is usually unnecessary because dependencies are auto-inferred from value references. Only add explicit dependsOn when a resolver must run after another but does NOT reference its value (pure ordering dependency).
The executor builds a DAG, topologically sorts into phases, then executes phases sequentially with concurrent execution within each phase.
ValueRef Format
Used everywhere a value can be dynamic (resolver inputs, action inputs, messages):
| Format | YAML | When to Use |
|---|
| Literal | key: value | Static values |
| Resolver | key: {rslvr: resolver-name} | Reference another resolver's output |
| CEL | key: {expr: "_.field.upperAscii()"} | Dynamic computation |
| Go Template | key: {tmpl: "Hello {{.name}}"} | Text rendering with template logic |
Decision Guide
- Literal: When the value is known at authoring time
- Resolver ref (
rslvr): When you need another resolver's raw output
- CEL (
expr): Data manipulation, conditionals, type coercion, list/map operations
- Go Template (
tmpl): Text rendering, multi-line output, file content generation
Action Workflow
Actions execute after all resolvers complete. They form their own DAG.
spec:
workflow:
resultSchemaMode: error
actions:
write-config:
provider: directory
inputs:
source: {rslvr: templates-dir}
destination: {expr: "_.output_path"}
dependsOn: []
when: "_.enabled"
continueOnError: false
timeout: 30s
exclusive: [other-write]
retry:
maxAttempts: 3
backoff: exponential
forEach:
source: "_.items"
item: __item
Action-Specific Context
__actions: Map of completed action results (keyed by action name)
- Each action result has:
success (bool), plus provider-specific fields
Testing
Solutions support functional tests via a separate tests.yaml composed into the solution:
apiVersion: scafctl.io/v1
kind: Tests
tests:
- name: basic-test
command: run solution
args: [-f, ./cldctl/solution.yaml]
inputs:
resolver-name: test-value
assertions:
- expr: "__output.resolver_name == 'expected'"
files:
- path: output/file.txt
contains: "expected content"
exitCode: 0
Compose into solution: compose: [tests.yaml]
Snapshot masking (golden baselines)
A test case with snapshot: <path> compares against a golden file. Built-in
presets (timestamp, uuid, sandbox) are always normalized. Declare masks
to normalize other volatile values; set snapshotSource: files to snapshot the
tree of rendered files instead of stdout:
cases:
golden-render:
command: [run, solution]
snapshot: testdata/snapshots/rendered.txt
snapshotSource: files
masks:
- name: entra-group
pattern: '\[[^\]]*\]'
placeholder: "<GROUP>"
path: "envs/**/*.auto.tfvars"
- use: email
- use: uuid
disabled: true
Any declared mask makes the test report a relaxed status (PASS*) with a
per-mask match count. Regenerate golden files with --update-snapshots.
Built-in Providers
| Provider | Capability | Purpose |
|---|
| parameter | from | Interactive user prompts |
| env | from | Environment variables |
| static | from | Hardcoded values |
| file | from | Read file contents |
| exec | from | Run external commands |
| shell | from | Shell command execution |
| http | from | HTTP requests |
| git | from | Git operations |
| github | from | GitHub API |
| secret | from | Secret management |
| identity | from | Auth identity tokens |
| metadata | from | Solution metadata access |
| solution | from | Cross-solution references |
| cel | transform | CEL expression evaluation |
| gotmpl | transform | Go template rendering |
| hcl | transform | HCL format conversion |
| validation | validation | Rule-based validation |
| message | action | User-facing messages |
| directory | action | Directory/file operations |
| debug | from | Debug output |
| sleep | from | Delay execution |
Key Types (pkg/spec/)
Solution: Top-level with APIVersion, Kind, Metadata, Catalog, Spec
Resolver: Name, Type, Phases (Resolve/Transform/Validate), When, DependsOn
ValueRef: Literal | Resolver | Expr | Tmpl
Workflow: Actions map, ResultSchemaMode
Action: Provider, Inputs, DependsOn, When, OnError, Retry, ForEach
Key Packages
pkg/spec/: YAML types and parsing
pkg/solution/: Solution loading and execution orchestration
pkg/resolver/: DAG building, phase execution, dependency extraction
pkg/action/: Action workflow execution
pkg/provider/: Provider interface and registry