Output Directory Tutorial#
This tutorial walks you through using the --output-dir flag to redirect action output to a specific directory. You’ll learn the phase-based directory model, how resolvers and actions differ in path handling, and how to reference the original working directory with __cwd.
Prerequisites#
- scafctl installed and available in your PATH
- Basic familiarity with YAML syntax and solution files
- Completion of the Getting Started tutorial
Table of Contents#
- Overview
- Phase-Based Directory Model
- Basic Usage
- Using
__cwdfor Original Directory Access - Combining with Dry-Run
- Per-Provider Usage
- Default via Configuration
- Absolute Paths Are Never Rewritten
- Affected Providers
- Common Patterns
Overview#
By default, actions resolve relative paths against the current working directory (CWD). The --output-dir flag changes this so actions write to a designated output directory instead, while resolvers continue reading from CWD.
This is useful when you want to:
- Keep your source directory clean while generating output elsewhere
- Write generated files to a build or staging directory
- Separate “read” (resolver) and “write” (action) phases to different locations
Phase-Based Directory Model#
scafctl execution has two phases with different directory semantics:
| Phase | Directory | Purpose |
|---|---|---|
| Resolvers | CWD (always) | Gather data — read files, query APIs, compute values |
| Actions | --output-dir (when set) | Produce output — write files, run commands, create directories |
When --output-dir is not set, actions also use CWD (fully backward compatible).
CWD (current working directory)
├── solution.yaml ← parsed by scafctl
├── templates/ ← read by resolvers
│ └── app.yaml.tpl
└── data/ ← read by resolvers
└── config.json
--output-dir /tmp/output
├── app.yaml ← written by actions
├── config/
│ └── settings.json ← written by actions
└── scripts/
└── deploy.sh ← written by actionsBasic Usage#
Create a solution with a resolver that reads from CWD and an action that writes to the output directory.
Step 1: Create Source Files#
Create a project directory with a source file:
mkdir -p output-dir-demo && cd output-dir-demo
cat > source.txt <<'EOF'
Hello from the source directory!
EOFStep 2: Create the Solution#
Create solution.yaml:
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: output-dir-demo
version: 1.0.0
spec:
resolvers:
# Resolvers always read from CWD
source-content:
type: any
resolve:
with:
- provider: file
inputs:
operation: read
path: source.txt
workflow:
actions:
# Actions write to --output-dir when set
write-output:
provider: file
inputs:
operation: write
path: result.txt
content:
rslvr: source-contentStep 3: Run with --output-dir#
scafctl run solution -f solution.yaml --output-dir /tmp/demo-outputscafctl run solution -f solution.yaml --output-dir /tmp/demo-outputStep 4: Verify#
# The output file is in the output directory, NOT in CWD
cat /tmp/demo-output/result.txt
# Output: Hello from the source directory!
# CWD is unchanged — no result.txt here
ls -la
# Only source.txt and solution.yamlThe resolver read source.txt from CWD. The action wrote result.txt to /tmp/demo-output.
Using __cwd for Original Directory Access#
When --output-dir is active, actions resolve relative paths against the output directory. But sometimes an action needs to reference the original working directory — for example, to run a script that lives alongside the solution file.
The __cwd built-in variable provides the original working directory path in action expressions.
Example: Reference a Script in CWD#
apiVersion: scafctl.io/v1
kind: Solution
metadata:
name: cwd-escape-demo
version: 1.0.0
spec:
resolvers: {}
workflow:
actions:
# Run a script from the original CWD
run-script:
provider: exec
inputs:
command: bash
args:
expr: '[__cwd + "/scripts/deploy.sh"]'
# Write a log that records where files came from
write-log:
provider: file
inputs:
operation: write
path: build-log.txt
content:
expr: >-
"Build completed\n" +
"Source directory: " + __cwd + "\n" +
"Output directory: " + __cwd + "/../output"Example: Copy a File via Content#
# Read from CWD in a resolver, write to output-dir in an action
spec:
resolvers:
readme:
type: any
resolve:
with:
- provider: file
inputs:
operation: read
path: README.md
workflow:
actions:
copy-readme:
provider: file
inputs:
operation: write
path: README.md
content:
rslvr: readmeBecause resolvers always use CWD and actions use --output-dir, this effectively copies README.md from CWD to the output directory.
Combining with Dry-Run#
Use --dry-run with --output-dir to preview what files would be written and where:
scafctl run solution -f solution.yaml --output-dir /tmp/demo-output --dry-runscafctl run solution -f solution.yaml --output-dir /tmp/demo-output --dry-runThis resolves all values and shows the action plan but doesn’t create any files or directories. Use this to verify paths before a real run.
Per-Provider Usage#
The --output-dir flag also works with run provider when the capability is action:
# Write a file to the output directory using the file provider directly
scafctl run provider file --capability action --output-dir /tmp/demo-output \
-i '{"operation": "write", "path": "hello.txt", "content": "world"}'
# Verify
cat /tmp/demo-output/hello.txt
# Output: world# Write a file to the output directory using the file provider directly
scafctl run provider file --capability action --output-dir /tmp/demo-output `
-i '{"operation": "write", "path": "hello.txt", "content": "world"}'
# Verify
cat /tmp/demo-output/hello.txt
# Output: world# Create a directory inside the output directory
scafctl run provider directory --capability action --output-dir /tmp/demo-output \
-i '{"operation": "mkdir", "path": "sub/nested"}'
# Verify
ls -la /tmp/demo-output/sub/nested# Create a directory inside the output directory
scafctl run provider directory --capability action --output-dir /tmp/demo-output `
-i '{"operation": "mkdir", "path": "sub/nested"}'
# Verify
ls -la /tmp/demo-output/sub/nestedThe flag only applies when --capability action is used. Resolver-mode provider runs ignore --output-dir:
# --output-dir is ignored for resolver capability (reads from CWD)
scafctl run provider file --capability resolver --output-dir /tmp/demo-output \
-i '{"operation": "read", "path": "source.txt"}'# --output-dir is ignored for resolver capability (reads from CWD)
scafctl run provider file --capability resolver --output-dir /tmp/demo-output `
-i '{"operation": "read", "path": "source.txt"}'Default via Configuration#
Instead of passing --output-dir every time, set a default in your scafctl configuration:
# In scafctl config (e.g., scafctl config set action.outputDir /path/to/output)
action:
outputDir: /path/to/default/outputThe CLI flag always overrides the configured default:
# Uses the configured default
scafctl run solution -f solution.yaml
# Overrides the configured default
scafctl run solution -f solution.yaml --output-dir /tmp/override# Uses the configured default
scafctl run solution -f solution.yaml
# Overrides the configured default
scafctl run solution -f solution.yaml --output-dir /tmp/overrideAbsolute Paths Are Never Rewritten#
When a provider input uses an absolute path, it is used as-is regardless of --output-dir:
workflow:
actions:
absolute-example:
provider: file
inputs:
operation: write
path: /etc/myapp/config.yaml # Absolute — not affected by --output-dir
content: "some config"
relative-example:
provider: file
inputs:
operation: write
path: config.yaml # Relative — resolves to output-dir
content: "some config"With --output-dir /tmp/output:
absolute-examplewrites to/etc/myapp/config.yamlrelative-examplewrites to/tmp/output/config.yaml
Affected Providers#
The following providers use --output-dir for path resolution in action mode:
| Provider | Operations Affected |
|---|---|
file | read, write, exists, delete, write-tree |
directory | list, mkdir, rmdir, copy |
exec | workingDir defaults to output-dir when empty |
hcl | parse, format, validate, list |
Providers like static, cel, http, and solution are unaffected because they don’t perform filesystem operations.
Exec Provider Special Behavior#
The exec provider treats workingDir specially:
- Empty
workingDirwith--output-dir→ commands run in the output directory - Relative
workingDirwith--output-dir→ resolved against the output directory - Absolute
workingDir→ always used as-is
workflow:
actions:
# Runs in the output directory (workingDir defaults to output-dir)
list-output:
provider: exec
inputs:
command: ls -la
# Runs in output-dir/subdir
list-subdir:
provider: exec
inputs:
command: ls -la
workingDir: subdir
# Runs in /tmp regardless of output-dir
list-tmp:
provider: exec
inputs:
command: ls -la
workingDir: /tmpCommon Patterns#
Generate a Project into a Target Directory#
spec:
resolvers:
config:
type: any
resolve:
with:
- provider: static
inputs:
value:
name: my-service
port: 8080
workflow:
actions:
write-dockerfile:
provider: file
inputs:
operation: write
path: Dockerfile
content:
expr: >-
"FROM golang:1.23\n" +
"EXPOSE " + string(_.config.port) + "\n" +
"CMD [\"./\" + _.config.name + \"]"
write-readme:
provider: file
inputs:
operation: write
path: README.md
content:
expr: '"# " + _.config.name + "\n\nPort: " + string(_.config.port)'
create-dirs:
provider: directory
inputs:
operation: mkdir
path: cmd/serverscafctl run solution -f solution.yaml --output-dir ./generated/my-servicescafctl run solution -f solution.yaml --output-dir ./generated/my-serviceCI/CD: Build Artifacts to a Staging Directory#
# In a CI pipeline, write outputs to a staging dir
scafctl run solution -f build.yaml --output-dir "$BUILD_DIR/artifacts"
# Then upload or deploy from that directory
aws s3 sync "$BUILD_DIR/artifacts" s3://my-bucket/# In a CI pipeline, write outputs to a staging dir
scafctl run solution -f build.yaml --output-dir "$BUILD_DIR/artifacts"
# Then upload or deploy from that directory
aws s3 sync "$BUILD_DIR/artifacts" s3://my-bucket/Template Rendering with Source Separation#
spec:
resolvers:
# Read template from CWD
template:
type: any
resolve:
with:
- provider: file
inputs:
operation: read
path: templates/deployment.yaml.tpl
values:
type: any
resolve:
with:
- provider: file
inputs:
operation: read
path: values.yaml
format: yaml
workflow:
actions:
# Write rendered output to output-dir
render:
provider: file
inputs:
operation: write
path: k8s/deployment.yaml
content:
tmpl: "{{ .template }}"# Templates stay in source, rendered output goes elsewhere
scafctl run solution -f solution.yaml --output-dir ./dist# Templates stay in source, rendered output goes elsewhere
scafctl run solution -f solution.yaml --output-dir ./distNext Steps#
- See the Output Directory design doc for implementation details
- Try the complete example for a working demonstration
- Learn about Dry-Run to preview output-dir behavior before executing
- Explore the Directory Provider Tutorial for more filesystem operations