Resolver Tutorial#
This tutorial walks you through using scafctl resolvers to dynamically resolve configuration values. You’ll learn how to define resolvers, use different providers, handle dependencies, and implement common patterns.
Prerequisites#
- scafctl installed and available in your PATH
- Basic familiarity with YAML syntax
- Understanding of environment variables
Table of Contents#
- Your First Resolver
- Using Parameters
- Resolver Dependencies
- Transformations
- Validation
- Conditional Execution
- Error Handling
- Working with HTTP APIs
- Common Patterns
Your First Resolver#
Let’s create a simple solution with one resolver that returns a static value.
Step 1: Create the Solution File#
Create a file called hello.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: hello-world
version: 1.0.0
spec:
resolvers:
greeting:
type: string
resolve:
with:
- provider: static
inputs:
value: "Hello, World!"Step 2: Run the Solution#
scafctl run resolver -f hello.yamlscafctl run resolver -f hello.yamlOutput:
╭─ scafctl run resolver ─╮
│KEY VALUE │
│─────────────────────── │
│greeting Hello, World! │
╰ _ ─────────── map: 1/1 ╯Tip: Add
-o jsonto get JSON output:scafctl run resolver -f hello.yaml -o json
Understanding the Structure#
- apiVersion/kind: Identifies this as a scafctl Solution
- metadata: Solution name, version, and description
- spec.resolvers: Map of resolver definitions
- greeting: The resolver name (used as the output key)
- type: Expected output type (optional, defaults to
any) - resolve.with: List of provider sources to try
Using Parameters#
Parameters let you pass values from the command line to your resolvers.
Step 1: Create a Parameterized Solution#
Create greet.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: parameterized-greeting
version: 1.0.0
spec:
resolvers:
name:
type: string
resolve:
with:
- provider: parameter
continueOnError: true
inputs:
key: user_name
- provider: static
inputs:
value: "World"
greeting:
type: string
resolve:
with:
- provider: cel
inputs:
expression: "'Hello, ' + _.name + '!'"Step 2: Run with Parameters#
scafctl run resolver -f greet.yamlscafctl run resolver -f greet.yamlOutput:
╭─ scafctl run resolver ─╮
│KEY VALUE │
│─────────────────────── │
│greeting Hello, World! │
│name World │
╰ _ ─────────── map: 1/2 ╯Pass a parameter:
scafctl run resolver -f greet.yaml -r user_name=Alicescafctl run resolver -f greet.yaml -r user_name=AliceOutput:
╭─ scafctl run resolver ─╮
│KEY VALUE │
│─────────────────────── │
│greeting Hello, Alice! │
│name Alice │
╰ _ ─────────── map: 1/2 ╯Using Parameter Files#
For complex parameter sets, use a parameter file:
Create params.yaml:
user_name: CharlieRun with the file:
scafctl run resolver -f greet.yaml -r @params.yaml# Wrap @file in single quotes to avoid splatting operator
scafctl run resolver -f greet.yaml -r '@params.yaml'You can also pipe parameters from stdin using @-:
echo '{"user_name": "Charlie"}' | scafctl run resolver -f greet.yaml -r @-
cat params.yaml | scafctl run resolver -f greet.yaml -r @-'{"user_name": "Charlie"}' | scafctl run resolver -f greet.yaml -r '@-'
Get-Content params.yaml | scafctl run resolver -f greet.yaml -r '@-'To pipe raw text into a single parameter key, use key=@-:
# Raw stdin into a single key (not parsed as YAML/JSON)
echo Charlie | scafctl run resolver -f greet.yaml -r user_name=@-
# Read a file's content into a key
echo -n "Charlie" > name.txt
scafctl run resolver -f greet.yaml -r user_name=@name.txt'Charlie' | scafctl run resolver -f greet.yaml -r 'user_name=@-'
# Create the file first
Set-Content -NoNewline -Path name.txt -Value 'Charlie'
scafctl run resolver -f greet.yaml -r 'user_name=@name.txt'Output:
╭─ scafctl run resolver ──╮
│KEY VALUE │
│─────────────────────────│
│greeting Hello, Charlie!│
│name Charlie │
╰ _ ──────────── map: 1/2 ╯Resolver Dependencies#
Resolvers can reference other resolvers using _.resolver_name syntax in CEL expressions.
For names with hyphens, use bracket notation: _["resolver-name"]. See the
CEL tutorial
for details.
Step 1: Create a Solution with Dependencies#
Create config.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: config-builder
version: 1.0.0
spec:
resolvers:
environment:
type: string
resolve:
with:
- provider: parameter
continueOnError: true
inputs:
key: env
- provider: static
inputs:
value: development
port:
type: int
resolve:
with:
- provider: parameter
continueOnError: true
inputs:
key: port
- provider: static
inputs:
value: 8080
base_url:
type: string
resolve:
with:
- provider: cel
inputs:
expression: |
_.environment == 'production'
? 'https://api.example.com'
: 'http://localhost:' + string(_.port)
config:
type: any
resolve:
with:
- provider: cel
inputs:
expression: |
{
'environment': _.environment,
'port': _.port,
'baseUrl': _.base_url,
'debug': _.environment != 'production'
}Step 2: Run and Observe Phases#
scafctl run resolver -f config.yaml --progressscafctl run resolver -f config.yaml --progressThe --progress flag shows how resolvers execute in phases based on dependencies:
[1] environment ✓ <time>
[1] port ✓ <time>
[2] base_url ✓ <time>
[3] config ✓ <time>Phase numbers in brackets show concurrent execution groups. Resolvers in the same phase run concurrently.
Dependency Rules#
- Dependencies are auto-inferred from value references (
expr:,rslvr:,tmpl:) – you do NOT need to declaredependsOnwhen a resolver references another by value - Resolvers in the same phase run concurrently
- A resolver waits for all its dependencies to complete
- Circular dependencies cause an error
- Only add explicit
dependsOnwhen a resolver must wait for another without referencing its value (pure ordering dependency)
Transformations#
Transform values after they’re resolved using the transform phase.
Example: String Manipulation#
Create a file called transform.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: transform-example
version: 1.0.0
spec:
resolvers:
raw_input:
type: string
resolve:
with:
- provider: parameter
continueOnError: true
inputs:
key: input
- provider: static
inputs:
value: " Hello World "
transform:
with:
- provider: cel
inputs:
expression: "__self.trim()"
- provider: cel
inputs:
expression: "__self.lowerAscii()"Key Concept: In the transform phase, __self refers to the current value being transformed. Each transform step receives the output of the previous step.
Run it:
scafctl run resolver -f transform.yaml -o jsonscafctl run resolver -f transform.yaml -o jsonOutput:
{
"raw_input": "hello world"
}Tip:
scafctl run resolver -o jsonoutputs only resolver values by default. Pass--show-executionto include__executionmetadata (phases, timing, provider info).
The value was trimmed of whitespace, then lowercased — each transform step feeds into the next.
Example: Data Enrichment#
Create a file called enrich.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: enrich-config
version: 1.0.0
spec:
resolvers:
base_config:
type: any
resolve:
with:
- provider: static
inputs:
value:
name: my-app
version: "1.0.0"
transform:
with:
# Add timestamp
- provider: cel
inputs:
expression: "map.merge(__self, {'timestamp': time.now()})"
# Add environment-specific settings
- provider: cel
inputs:
expression: "map.merge(__self, {'debug': true, 'logLevel': 'info'})"Run it:
scafctl run resolver -f enrich.yaml -o jsonscafctl run resolver -f enrich.yaml -o jsonOutput (timestamp will vary):
{
"base_config": {
"debug": true,
"logLevel": "info",
"name": "my-app",
"timestamp": "2026-02-16T12:00:00.000000-05:00",
"version": "1.0.0"
}
}Validation#
Validate resolved values to ensure they meet requirements.
Example: Port Range Validation#
Create a file called validated-config.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: validated-config
version: 1.0.0
spec:
resolvers:
port:
type: int
resolve:
with:
- provider: parameter
continueOnError: true
inputs:
key: port
- provider: static
inputs:
value: 8080
validate:
with:
- provider: validation
inputs:
expression: "__self >= 1024 && __self <= 65535"
message: "Port must be between 1024 and 65535"Run it with a valid port:
scafctl run resolver -f validated-config.yaml -r port=8080 -o jsonscafctl run resolver -f validated-config.yaml -r port=8080 -o jsonOutput:
{
"port": 8080
}Run it with an invalid port to see the validation error:
scafctl run resolver -f validated-config.yaml -r port=80scafctl run resolver -f validated-config.yaml -r port=80Output:
❌ resolver execution failed: ... validation: Port must be between 1024 and 65535Example: Multiple Validations#
Create a file called email-validator.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: email-validator
version: 1.0.0
spec:
resolvers:
email:
type: string
resolve:
with:
- provider: parameter
continueOnError: true
inputs:
key: email
- provider: static
inputs:
value: "user@example.com"
validate:
with:
- provider: validation
inputs:
match: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
message: "Invalid email format"
- provider: validation
inputs:
expression: "!__self.endsWith('.test')"
message: "Test emails not allowed"Note: All validation rules run and errors are aggregated. You’ll see all failures, not just the first one.
Run it:
scafctl run resolver -f email-validator.yaml -o jsonscafctl run resolver -f email-validator.yaml -o jsonOutput:
{
"email": "user@example.com"
}Now try an invalid value that fails both validations — not a valid email format and ends with .test:
scafctl run resolver -f email-validator.yaml -r email="not-an-email.test" -o jsonscafctl run resolver -f email-validator.yaml -r email="not-an-email.test" -o jsonOutput:
❌ resolver execution failed: phase 1 failed: resolver "email" failed: resolver "email" validation failed with 2 errors:
- [rule 1] validation: Invalid email format
- [rule 2] validation: Test emails not allowedBoth validation errors are reported together rather than failing on the first one.
Example: Conditional Validation Rule#
Each rule in validate.with can carry its own when condition. When the
condition evaluates to false, that single rule is skipped while the other
rules still run. The condition can reference __self (the resolved value) and
any other resolver via _. This is useful when a rule only applies to certain
values – for example, only enforcing a strict pattern in production.
Create a file called conditional-validation.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: conditional-validation
version: 1.0.0
spec:
resolvers:
appName:
type: string
resolve:
with:
- provider: parameter
continueOnError: true
inputs:
key: appName
- provider: static
inputs:
value: my-app
validate:
with:
# Always enforced: lowercase, alphanumeric with hyphens.
- provider: validation
inputs:
match: "^[a-z0-9-]+$"
message: "Name must be lowercase alphanumeric with hyphens"
# Only enforced in production: must start with "prod-".
- provider: validation
when:
expr: "_.environment == 'production'"
inputs:
match: "^prod-"
message: "Production app names must start with 'prod-'"
environment:
type: string
resolve:
with:
- provider: parameter
continueOnError: true
inputs:
key: env
- provider: static
inputs:
value: developmentIn development (the default), the second rule is skipped, so my-app passes.
In production, the second rule runs and a name without the prod- prefix
fails. Put the when on the individual rule inside validate.with, not on the
validate phase as a whole (a phase-level validate.when gates every rule at
once).
Conditional Execution#
Skip resolvers or phases based on conditions.
Resolver-Level Condition#
Create a file called conditional.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: conditional-example
version: 1.0.0
spec:
resolvers:
environment:
type: string
resolve:
with:
- provider: parameter
continueOnError: true
inputs:
key: env
- provider: static
inputs:
value: development
# Only runs in production
prod_secrets:
when:
expr: "_.environment == 'production'"
type: string
resolve:
with:
- provider: static
inputs:
value: "prod-secret-value"Run it with development (default) — the prod_secrets resolver is skipped:
scafctl run resolver -f conditional.yaml -o jsonscafctl run resolver -f conditional.yaml -o jsonOutput (only environment is resolved; prod_secrets is skipped):
{
"environment": "development"
}Run it with production — the prod_secrets resolver executes:
scafctl run resolver -f conditional.yaml -r env=production -o jsonscafctl run resolver -f conditional.yaml -r env=production -o jsonOutput:
{
"environment": "production",
"prod_secrets": "prod-secret-value"
}Phase-Level Condition#
Create a file called phase-condition.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: phase-condition
version: 1.0.0
spec:
resolvers:
feature_flags:
type: any
resolve:
with:
- provider: static
inputs:
value:
enable_transform: true
transform:
with:
- provider: cel
when:
expr: "__self.enable_transform == true"
inputs:
expression: "map.merge(__self, {'transformed': true})"Run it:
scafctl run resolver -f phase-condition.yaml -o jsonscafctl run resolver -f phase-condition.yaml -o jsonOutput:
{
"feature_flags": {
"enable_transform": true,
"transformed": true
}
}Error Handling#
Handle errors gracefully with fallback sources.
Fallback Pattern#
Create a file called fallback.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: fallback-example
version: 1.0.0
spec:
resolvers:
config:
type: any
resolve:
with:
# Try remote config first
- provider: http
inputs:
url: https://config.example.com/settings
timeout: 5s
# Fall back to local file
- provider: file
inputs:
operation: read
path: ./config.json
# Last resort: default values
- provider: static
inputs:
value:
debug: false
timeout: 30Run it (the HTTP and file providers will fail, so it falls back to static):
scafctl run resolver -f fallback.yaml -o jsonscafctl run resolver -f fallback.yaml -o jsonOutput:
{
"config": {
"debug": false,
"timeout": 30
}
}Controlling fallback with continueOnError (resolve phase):
The resolve phase falls through to the next source by default. Use
continueOnError to control this explicitly. It accepts a boolean or a CEL
condition (truthy recovers, falsy fails):
continueOnError: true(resolve default): Try the next source in the list. The resolve phase acts as an implicit fallback chain.continueOnError: false: Stop execution immediately and return the error without trying remaining sources.
The legacy
onErrorenum (continue/fail) is deprecated but still works. MigrateonError: continuetocontinueOnError: trueandonError: failtocontinueOnError: false. When both are set,continueOnErrorwins and the linter reports an error.
Conditional Recovery with continueOnError#
A boolean continueOnError is unconditional: it either always recovers or
always fails. Pass a CEL condition instead to recover only for specific
errors and fail for everything else. The condition is evaluated when the source
(or transform step) fails, with the error text bound as __error:
- If the condition is truthy, the error is recovered (resolve: try the next source; transform: skip the step and keep the current value).
- If the condition is falsy, the resolver fails.
spec:
resolvers:
config:
type: any
resolve:
with:
# Recover only on transient timeouts; fail fast on anything else
# (auth errors, bad config, etc.) instead of silently falling back.
- provider: http
inputs:
url: https://config.example.com/settings
timeout: 5s
continueOnError: __error.contains("timeout")
- provider: static
inputs:
value:
debug: falseInside continueOnError the error text is available as the top-level variable
__error (the same way __self is exposed in transform/validate conditions).
The condition must evaluate to a boolean.
Custom Error Messages#
Use the messages field to replace default error text with user-friendly messages. The messages.error field supports static strings, CEL expressions (expr:), and Go templates (tmpl:):
# messages-error.yaml
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: custom-error-messages
version: 1.0.0
spec:
resolvers:
port:
description: Application port number
messages:
error: "Port must be between 1024 and 65535. Got: {{ .__error }}"
resolve:
with:
- provider: parameter
inputs:
name: port
validate:
with:
- provider: validation
inputs:
rules:
- expr: "int(__self) >= 1024 && int(__self) <= 65535"
message: invalid port range
apiEndpoint:
description: API endpoint URL
messages:
error:
expr: "'Failed to reach API endpoint. Error: ' + __error"
resolve:
with:
- provider: http
inputs:
url: https://api.example.com/health
method: GETThe messages.error value is evaluated when any phase fails (resolve, transform, or validate). Two variables are available:
| Variable | Description |
|---|---|
_ | The resolver data map (all resolved values so far) |
__error | The original error message string |
When a custom error message is set, it replaces the default error text in all output — CLI, MCP tools, and snapshot captures.
Working with HTTP APIs#
Fetch configuration from remote APIs.
Basic HTTP Request#
Create a file called http-example.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: http-example
version: 1.0.0
spec:
resolvers:
api_data:
type: any
resolve:
with:
- provider: http
inputs:
url: https://httpbin.org/get
method: GET
headers:
Accept: application/json
timeout: 10sRun it:
scafctl run resolver -f http-example.yaml -o jsonscafctl run resolver -f http-example.yaml -o jsonOutput (body and headers will vary):
{
"api_data": {
"body": "...",
"headers": { "...": "..." },
"statusCode": 200,
"success": true
}
}The success field is true for any 2xx status by default. A non-2xx
response (for example 404) does not raise an error – the resolver still
produces a value with success: false so you can branch on it in a transform or
a downstream resolver.
Accepting Non-2xx Status Codes#
Sometimes a non-2xx status is a normal result. For example, a lookup that
returns 404 when a resource does not yet exist. Use acceptableStatusCodes to
declare which statuses count as successful. Each entry may be an exact integer
(200), a class shorthand ("2xx"), or an inclusive range ("200-204"):
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: optional-lookup
version: 1.0.0
spec:
resolvers:
existing:
type: any
resolve:
with:
- provider: http
inputs:
url: https://api.example.com/users/maybe-missing
method: GET
autoParseJson: true
acceptableStatusCodes: [200, 404]With acceptableStatusCodes set, a 404 yields success: true and the body is
still returned (and parsed when autoParseJson is enabled). Any status outside
the set – such as 500 – fails the source, which lets a continueOnError: true
policy fall back to the next source:
spec:
resolvers:
config:
type: any
resolve:
with:
# Fails on any status other than 2xx or 404, falling through below.
- provider: http
continueOnError: true
inputs:
url: https://api.example.com/config
method: GET
autoParseJson: true
acceptableStatusCodes: [200, 404]
- provider: static
inputs:
value: { fallback: true }With Authentication#
Create a file called auth-api.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: authenticated-api
version: 1.0.0
spec:
resolvers:
api_token:
sensitive: true # Redact in table output
resolve:
with:
- provider: env
inputs:
operation: get
name: API_TOKEN
api_data:
type: any
resolve:
with:
- provider: http
inputs:
url: https://httpbin.org/headers
headers:
Authorization:
tmpl: "Bearer {{.api_token}}"Run it (requires the API_TOKEN environment variable to be set):
export API_TOKEN=your-token-here
scafctl run resolver -f auth-api.yaml -o json$env:API_TOKEN = "your-token-here"
scafctl run resolver -f auth-api.yaml -o jsonCommon Patterns#
The following patterns are complete, self-contained solution files. Create a new file for each pattern to try it out.
Pattern 1: Environment-Based Configuration#
Create a file called env-config.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: env-config
version: 1.0.0
spec:
resolvers:
environment:
type: string
resolve:
with:
- provider: parameter
continueOnError: true
inputs:
key: env
- provider: static
inputs:
value: development
database_url:
type: string
sensitive: true
resolve:
with:
- provider: cel
inputs:
expression: |
_.environment == 'production'
? 'postgres://prod-db.example.com:5432/app'
: 'postgres://localhost:5432/app_dev'scafctl run resolver -f env-config.yamlscafctl run resolver -f env-config.yamlOutput:
╭─ scafctl run resolver ─╮
│KEY VALUE │
│─────────────────────── │
│database_url [REDACTED]│
│environment development│
╰ _ ─────────── map: 1/2 ╯Note: Fields marked
sensitive: trueare shown as[REDACTED]in table output.
Structured output (JSON, YAML) reveals sensitive values for machine consumption:
scafctl run resolver -f env-config.yaml -o jsonscafctl run resolver -f env-config.yaml -o jsonOutput:
{
"database_url": "postgres://localhost:5432/app_dev",
"environment": "development"
}Use --show-sensitive to reveal values in table output:
scafctl run resolver -f env-config.yaml --show-sensitivescafctl run resolver -f env-config.yaml --show-sensitiveSensitive Redaction Behavior: Sensitive values are redacted in table/interactive output (human-facing) but revealed in JSON/YAML output (machine-facing), following the same model as Terraform. Use
--show-sensitiveto reveal values in all output formats.
Pattern 2: Feature Toggles#
Create a file called feature-toggles.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: feature-toggles
version: 1.0.0
spec:
resolvers:
features:
type: any
resolve:
with:
- provider: http
continueOnError: true
inputs:
url: https://features.example.com/api/flags
- provider: static
inputs:
value:
new_ui: false
dark_mode: true
ui_config:
type: any
resolve:
with:
- provider: cel
inputs:
expression: |
{
'theme': _.features.dark_mode ? 'dark' : 'light',
'version': _.features.new_ui ? 'v2' : 'v1'
}scafctl run resolver -f feature-toggles.yaml -o jsonscafctl run resolver -f feature-toggles.yaml -o jsonOutput:
{
"features": {
"dark_mode": true,
"new_ui": false
},
"ui_config": {
"theme": "dark",
"version": "v1"
}
}Pattern 3: Secret Management#
Create a file called secrets.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: secrets
version: 1.0.0
spec:
resolvers:
db_password:
type: string
sensitive: true
resolve:
with:
# In practice, use env or file providers here
# with continueOnError: true to fall through
- provider: static
inputs:
value: "default-dev-password"
connection_string:
sensitive: true
type: string
resolve:
with:
- provider: cel
inputs:
expression: "'postgres://app:' + _.db_password + '@db.example.com:5432/app'"scafctl run resolver -f secrets.yamlscafctl run resolver -f secrets.yamlOutput:
╭─ scafctl run resolver ──────╮
│KEY VALUE │
│──────────────────────────── │
│connection_string [REDACTED] │
│db_password [REDACTED] │
╰ _ ──────────────── map: 1/2 ╯Note: Both resolvers are marked
sensitive: true, so their values are redacted in table output.
Structured output reveals the actual values:
scafctl run resolver -f secrets.yaml -o jsonscafctl run resolver -f secrets.yaml -o jsonOutput:
{
"connection_string": "postgres://app:default-dev-password@db.example.com:5432/app",
"db_password": "default-dev-password"
}Tip: Use table output (the default) when sharing your screen or in CI logs to avoid accidentally exposing secrets. Use
-o jsonor-o yamlwhen piping to downstream tools that need the actual values.
Pattern 4: Multi-Stage Pipeline#
Create a file called pipeline.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: data-pipeline
version: 1.0.0
spec:
resolvers:
raw_data:
type: any
resolve:
with:
- provider: static
inputs:
value:
- id: 1
name: "Alice"
email: "alice@example.com"
active: true
- id: 2
name: "Bob"
email: "bob@example.com"
active: false
- id: 3
name: "Charlie"
email: "charlie@example.com"
active: true
- id: 4
name: "Diana"
email: "diana@example.com"
active: true
parsed_data:
type: any
resolve:
with:
- provider: cel
inputs:
expression: "_.raw_data"
transform:
with:
# Filter active users
- provider: cel
inputs:
expression: "__self.filter(u, u.active == true)"
# Select only needed fields
- provider: cel
inputs:
expression: "__self.map(u, {'id': u.id, 'name': u.name, 'email': u.email})"
validate:
with:
- provider: validation
inputs:
expression: "size(__self) > 0"
message: "No active users found"scafctl run resolver -f pipeline.yaml -o jsonscafctl run resolver -f pipeline.yaml -o jsonOutput:
{
"parsed_data": [
{ "email": "alice@example.com", "id": 1, "name": "Alice" },
{ "email": "charlie@example.com", "id": 3, "name": "Charlie" },
{ "email": "diana@example.com", "id": 4, "name": "Diana" }
],
"raw_data": [
{ "active": true, "email": "alice@example.com", "id": 1, "name": "Alice" },
{ "active": false, "email": "bob@example.com", "id": 2, "name": "Bob" },
{ "active": true, "email": "charlie@example.com", "id": 3, "name": "Charlie" },
{ "active": true, "email": "diana@example.com", "id": 4, "name": "Diana" }
]
}Bob was filtered out of parsed_data because active was false. The raw_data resolver is also included since run resolver returns all resolvers by default.
Array Iteration with forEach#
Transform Phase forEach#
When a resolver produces an array, the transform.with.forEach clause processes each element independently and collects results back into an array:
Save this as foreach-demo.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: foreach-demo
version: 1.0.0
spec:
resolvers:
doubled:
type: array
resolve:
with:
- provider: static
inputs:
value: [1, 2, 3, 4, 5]
transform:
with:
- provider: cel
forEach:
item: num # alias for the current element
index: i # alias for the current index
inputs:
expression: "num * 2"Run it to see the result:
scafctl run resolver doubled -f foreach-demo.yaml -o jsonscafctl run resolver doubled -f foreach-demo.yaml -o jsonFiltering with when and forEach#
By default, items where the when condition evaluates to false are removed from the output array. This makes forEach + when a natural filter:
transform:
with:
- provider: cel
forEach:
item: num
when:
expr: "num % 2 == 0" # only even numbers
inputs:
expression: "num * 2"Input [1, 2, 3, 4, 5] → output [4, 8] (only even numbers, doubled).
To retain index alignment (nil in place of skipped items), set keepSkipped: true:
forEach:
item: num
keepSkipped: true # output: [nil, 4, nil, 8, nil]Transform Phase forEach with Filtering#
To filter an array produced by a resolver, use forEach in the transform phase with a when condition:
Save this as foreach-filter-demo.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: foreach-filter-demo
version: 1.0.0
spec:
resolvers:
allUsers:
type: array
resolve:
with:
- provider: static
inputs:
value:
- {name: Alice, active: true}
- {name: Bob, active: false}
- {name: Carol, active: true}
activeUsers:
type: array
resolve:
with:
- provider: cel
inputs:
expression: _.allUsers
transform:
with:
- provider: cel
forEach:
item: user
when:
expr: 'user.active == true'
inputs:
expression: 'user'By default, items where the when condition is false are removed from the output array. The result contains only matched items:
[{"name": "Alice", "active": true}, {"name": "Carol", "active": true}]To retain nil entries for skipped items (preserving index alignment), set keepSkipped: true:
forEach:
item: user
keepSkipped: true # output: [{...Alice...}, nil, {...Carol...}]Run it:
scafctl run resolver activeUsers -f foreach-filter-demo.yaml -o jsonscafctl run resolver activeUsers -f foreach-filter-demo.yaml -o jsonTroubleshooting#
Common Issues#
Circular dependency error
Error: circular dependency detected: a -> b -> aSolution: Refactor to break the cycle, possibly by combining resolvers.
Type coercion error
Error: cannot coerce "hello" to intSolution: Ensure your provider returns a value compatible with the declared type.
Timeout error
Error: resolver "slow_api" timed out after 30sSolution: Increase the timeout in the resolver definition:
timeout: 60sValidation failed
Error: validation failed: Port must be between 1024 and 65535Solution: Check your input values meet the validation requirements.
Next Steps#
- Run Resolver Tutorial — Debug and inspect resolver execution
- Run Provider Tutorial — Test providers in isolation
- Actions Tutorial — Learn about workflows
- CEL Expressions Tutorial — Master CEL expressions and extension functions
- Reference-Data Lookups – Resolve values from curated taxonomy datasets
- Provider Reference — Complete provider documentation