Provider Reference#

This document provides a reference for all providers available in scafctl.

Note: All YAML examples in this reference show only the relevant resolver or action snippet. To use them, place each snippet inside a complete solution file with apiVersion, kind, metadata, and spec sections. See the Getting Started tutorial for the full solution structure.

Overview#

Providers are execution primitives used by resolvers and actions. Each provider has capabilities that determine where it can be used:

CapabilityUsed InDescription
fromResolver resolve.withFetch or generate data
transformResolver transform.withTransform data
validationResolver validate.withValidate data
actionAction providerPerform side effects
authenticationHTTP authProvide authentication

Built-in vs Official Plugin Providers#

scafctl providers are split into two categories:

CategoryDescription
Built-inCompiled into the scafctl binary. Always available without network access or plugins.
Official (plugin)Distributed as external plugin binaries via ghcr.io/oakwood-commons/providers/<name>. Auto-resolved at runtime when referenced in a solution.

Built-in providers (11): cel, debug, file, go-template, http, message, parameter, solution, state, static, validation

Official plugin providers (10): directory, env, exec, git, github, hcl, identity, metadata, secret, sleep

Official providers are automatically fetched from the catalog when a solution references them – no bundle.plugins declaration is required for local development. The run provider command also auto-resolves official providers (e.g., scafctl run provider exec command='ls'). For CI/CD and reproducible builds, declare them explicitly in bundle.plugins or use the --strict flag. See Plugin Auto-Fetching for details.

Capabilities Matrix#

ProviderTypefromtransformvalidationaction
celbuilt-in
debugbuilt-in
directoryofficial
envofficial
execofficial
filebuilt-in
gitofficial
githubofficial
go-templatebuilt-in
hclofficial
httpbuilt-in
identityofficial
messagebuilt-in
metadataofficial
parameterbuilt-in
secretofficial
sleepofficial
solutionbuilt-in
statebuilt-in
staticbuilt-in
validationbuilt-in

cel#

Transform and evaluate data using CEL (Common Expression Language) expressions.

Capabilities#

transform, action

Inputs#

FieldTypeRequiredDescription
expressionstringCEL expression to evaluate. Resolver data available under _
variablesanyAdditional variables for the CEL context

Output#

Returns the evaluation result (any type).

Examples#

# Transform: uppercase a string
transform:
  with:
    - provider: cel
      inputs:
        expression: "__self.toUpperCase()"

# Action: compute a value
provider: cel
inputs:
  expression: "_.items.map(i, i.price).sum()"

debug#

Debugging provider for inspecting resolver data during workflow execution.

Capabilities#

from, transform, validation, action

Inputs#

FieldTypeRequiredDescription
expressionstringCEL expression to filter/transform data before output
labelstringLabel or message for debug output context
formatstringOutput format: yaml, json, pretty (default: yaml)
destinationstringWhere to output: stdout, stderr, file (default: stdout)
pathstringFile path when destination is file
colorizeboolWhether to colorize terminal output

Examples#

# Debug all resolver data
resolve:
  with:
    - provider: debug
      inputs:
        label: "Resolver Context"

# Debug specific value
transform:
  with:
    - provider: debug
      inputs:
        expression: "_.config"
        format: json

directory#

Directory operations: listing contents with filtering, creating, removing, and copying directories.

Capabilities#

from, action

Inputs#

FieldTypeRequiredDescription
operationstringOperation: list, mkdir, rmdir, copy
pathstringTarget directory path (absolute or relative)
recursiveboolEnable recursive directory traversal (default: false)
maxDepthintMaximum recursion depth, 1–50 (default: 10)
includeContentboolRead and include file contents in output (default: false)
maxFileSizeintMaximum file size in bytes for content reading (default: 1048576)
filterGlobstringGlob pattern to filter entries (e.g., *.go). Mutually exclusive with filterRegex
filterRegexstringRegex to filter entry names. Mutually exclusive with filterGlob
excludeHiddenboolExclude hidden files/directories (names starting with .)
checksumstringCompute checksum for files: md5, sha256, sha512 (requires includeContent)
createDirsboolCreate parent directories for mkdir (like mkdir -p)
destinationstringDestination path for copy operation
forceboolForce removal of non-empty directories for rmdir

Output (list)#

FieldTypeDescription
entriesarrayList of directory entries
entries[].pathstringRelative path from the listed directory
entries[].absolutePathstringAbsolute filesystem path
entries[].namestringFile or directory name
entries[].extensionstringFile extension including dot (e.g., .go)
entries[].sizeintSize in bytes
entries[].isDirboolWhether entry is a directory
entries[].typestringEntry type: file or dir
entries[].modestringFile permission mode (e.g., 0644)
entries[].modTimestringLast modification time (RFC3339)
entries[].mimeTypestringMIME type based on extension
entries[].contentstringFile content (when includeContent is true)
entries[].contentEncodingstringtext or base64
entries[].checksumstringFile checksum (when checksum is specified)
entries[].checksumAlgorithmstringAlgorithm used
totalCountintTotal number of entries
dirCountintNumber of directories
fileCountintNumber of files
totalSizeintTotal size of all files in bytes
basePathstringAbsolute path of the listed directory

Output (mkdir, rmdir, copy)#

FieldTypeDescription
successboolWhether the operation succeeded
operationstringOperation that was performed
pathstringAbsolute path of the target directory

Examples#

# List directory contents
resolve:
  with:
    - provider: directory
      inputs:
        operation: list
        path: ./src

# Recursively find all Go files
resolve:
  with:
    - provider: directory
      inputs:
        operation: list
        path: ./pkg
        recursive: true
        filterGlob: "*.go"
        excludeHidden: true

# List with file contents and checksums
resolve:
  with:
    - provider: directory
      inputs:
        operation: list
        path: ./config
        recursive: true
        includeContent: true
        filterGlob: "*.yaml"
        checksum: sha256
        maxFileSize: 524288

# Create nested directory structure
provider: directory
inputs:
  operation: mkdir
  path: ./output/reports/2026
  createDirs: true

# Force-remove a directory
provider: directory
inputs:
  operation: rmdir
  path: ./tmp/build-output
  force: true

# Copy a directory tree
provider: directory
inputs:
  operation: copy
  path: ./config
  destination: ./config-backup

Security#

The directory provider enforces several security measures:

  • Recursion depth limit: The maxDepth input is clamped to 1–50 (default: 10). This prevents unbounded filesystem traversal that could cause resource exhaustion.
  • Symlink skipping: Symbolic links are silently skipped during listing to prevent symlink escape attacks where a symlink points outside the intended directory tree.
  • File size limit: When includeContent is enabled, maxFileSize (default: 1 MB) caps the content read per file to prevent memory exhaustion from large files.

env#

Read environment variables.

Capabilities#

from

Inputs#

FieldTypeRequiredDescription
operationstringOperation: get, list
namestringVariable name (required for get)
defaultstringDefault value if variable not set
prefixstringFilter variables by prefix (for list)

Output#

FieldTypeDescription
valuestringVariable value (for get)
variablesmapKey-value pairs (for list)
foundboolWhether the variable exists

Examples#

# Get environment variable with default
resolve:
  with:
    - provider: env
      inputs:
        operation: get
        name: DATABASE_URL
        default: "postgres://localhost/dev"

# List all vars with prefix
resolve:
  with:
    - provider: env
      inputs:
        operation: list
        prefix: "APP_"

Security#

The env provider enforces several security measures:

  • Mandatory prefix for list: The list operation requires a non-empty prefix input. Listing all environment variables without a scope would expose process secrets such as tokens, API keys, and database credentials.
  • No credential-bearing variables in output: By forcing a prefix scope, the provider prevents accidental enumeration of variables like AWS_SECRET_ACCESS_KEY, DATABASE_URL, or GITHUB_TOKEN that happen to exist in the process environment.

exec#

Execute shell commands using an embedded cross-platform POSIX shell interpreter. Commands work identically on Linux, macOS, and Windows without requiring external shell binaries. Supports pipes, redirections, variable expansion, command substitution, and common coreutils on all platforms. Optionally use external shells (bash, pwsh, cmd) for platform-specific features.

Capabilities#

from, transform, action

Inputs#

FieldTypeRequiredDefaultDescription
commandstringCommand to execute. Supports POSIX shell syntax including pipes, redirections, variable expansion, and command substitution by default
argsarrayAdditional arguments appended to the command. Arguments are automatically shell-quoted for safety
stdinstringStandard input to provide to the command
workingDirstringWorking directory for command execution
envobjectEnvironment variables to set (key-value pairs). Merged with the parent process environment. NO_COLOR=1 and TERM=dumb are injected automatically to prevent child processes from emitting ANSI escape codes in captured output (override by setting them explicitly)
timeoutintTimeout in seconds (0 or omit for no timeout, max 3600)
shellstringautoShell interpreter to use: auto (embedded POSIX shell — works on all platforms), sh (alias for auto), bash (external bash), pwsh (external PowerShell Core), cmd (external cmd.exe — Windows only)

Output#

FieldTypeDescription
stdoutstringStandard output
stderrstringStandard error
exitCodeintExit code
successboolWhether command succeeded (exit code 0) — action capability only
commandstringThe full command that was executed
shellstringThe shell interpreter that was used

Shell Modes#

ValueDescriptionPlatform
autoEmbedded POSIX shell (default). Pure Go — no external shell binary required. Supports pipes, redirections, variable expansion, command substitution, and Go-native coreutils on Windows.All
shAlias for autoAll
bashExternal bash binary from $PATH. Use for bash-specific features (globstar, arrays, etc.)Linux, macOS
pwshExternal PowerShell Core from $PATH. Use for PowerShell cmdlets and Windows administrationAll (where pwsh is installed)
cmdExternal cmd.exe. Use for Windows batch commandsWindows

Examples#

# Simple command — pipes and shell features work by default
provider: exec
inputs:
  command: "echo 'Hello, World!'"

# Command with arguments (automatically shell-quoted)
provider: exec
inputs:
  command: "echo"
  args: ["Hello", "World"]

# Shell pipeline — works on all platforms
provider: exec
inputs:
  command: "echo 'hello world' | tr a-z A-Z"

# With environment variables and working directory
provider: exec
inputs:
  command: "./deploy.sh"
  workingDir: "/opt/app"
  env:
    ENVIRONMENT: production
  timeout: 300

# PowerShell command
provider: exec
inputs:
  command: "Get-ChildItem | Select-Object Name"
  shell: pwsh

# External bash for bash-specific features
provider: exec
inputs:
  command: "shopt -s globstar; echo **/*.go"
  shell: bash

Security#

The exec provider enforces several security measures:

  • Lint rule for command injection: The exec-command-injection lint rule emits a warning when the command input uses a dynamic expression (expr) or template (tmpl). Shell metacharacters in resolved values can escape the intended command and execute arbitrary code.
  • Shell-quoted arguments: The args input automatically shell-quotes each argument before appending it to the command. Always pass dynamic/user-controlled data via args instead of interpolating it into the command string.
# BAD — dynamic value in command string risks injection
provider: exec
inputs:
  command:
    expr: "'echo ' + _.userInput"

# GOOD — static command, dynamic value via args (shell-quoted)
provider: exec
inputs:
  command: echo
  args:
    - expr: "_.userInput"

file#

Filesystem operations: read, write, check existence, delete, and batch write a tree of files.

Capabilities#

from, transform, action

Inputs#

FieldTypeRequiredDescription
operationstringOperation: read, write, exists, delete, write-tree
pathstringFile path — required for read, write, exists, delete
contentstringContent to write (required for write)
createDirsboolCreate parent directories if missing (for write)
encodingstringFile encoding: utf-8, binary (default: utf-8)
parsestringParse file content into a structured object (for read): json, yaml, or auto (decoder chosen by file extension .json/.yaml/.yml, else content-sniffed). Malformed content errors. Omitted returns raw content only. Ignored when encoding is binary.
basePathstringDestination root directory (required for write-tree)
entriesarrayArray of {path, content} objects (required for write-tree)
outputPathstringGo template to transform each entry’s path before writing (write-tree only). Available variables: __filePath, __fileName, __fileStem, __fileExtension, __fileDir. Sprig functions supported.

Output#

FieldTypeDescription
contentstringFile content (for read)
objectanyParsed content (for read, only when parse is set)
existsboolWhether file exists
sizeintFile size in bytes
successboolOperation success (action only)
operationstringOperation performed (write-tree only)
basePathstringResolved base path (write-tree only)
filesWrittenintNumber of files written (write-tree only)
pathsarrayRelative paths of files written (write-tree only)

Examples#

# Read file
resolve:
  with:
    - provider: file
      inputs:
        operation: read
        path: "./config.json"

# Write file
provider: file
inputs:
  operation: write
  path: "./output/result.txt"
  content:
    expr: "_.processedData"
  createDirs: true

# Check if file exists
resolve:
  with:
    - provider: file
      inputs:
        operation: exists
        path: "./optional-config.yaml"

# Write a tree of rendered files, stripping .tpl extensions
provider: file
inputs:
  operation: write-tree
  basePath: ./output
  entries:
    rslvr: rendered
  outputPath: >-
    {{ if .__fileDir }}{{ .__fileDir }}/{{ end }}{{ .__fileStem }}

Security#

The file provider enforces several security measures:

  • Path traversal protection: The write-tree operation validates that every output file resolves to a path inside the declared basePath. Entries containing .. segments that escape the base directory are rejected with an error.
  • outputPath traversal protection: When outputPath is specified, the final templated path is also validated to remain within basePath. This prevents a crafted Go template from writing files outside the intended output directory.

git#

Git version control operations.

Capabilities#

from, action

Inputs#

FieldTypeRequiredDescription
operationstringOperation: clone, pull, status, add, commit, push, checkout, branch, log, tag
urlstringRepository URL (for clone)
pathstringLocal repository path
branchstringBranch name
messagestringCommit message
filesarrayFiles to add
tagstringTag name
remotestringRemote name (default: origin)
depthintClone depth for shallow clone
usernamestringUsername for authentication
passwordstringPassword/token (secret)
forceboolForce the operation

Examples#

# Clone repository
provider: git
inputs:
  operation: clone
  url: https://github.com/org/repo.git
  path: ./repo
  depth: 1

# Commit and push
provider: git
inputs:
  operation: commit
  path: ./repo
  message:
    expr: "'Release ' + _.version"

# Then push
provider: git
inputs:
  operation: push
  path: ./repo

Security#

The git provider enforces several security measures:

  • Credential isolation: Authentication credentials are passed via a temporary .netrc file with 0600 permissions instead of being embedded in command-line arguments. This prevents credentials from being exposed via ps, /proc, or audit logs.
  • Netrc injection prevention: Username and password inputs are validated to reject whitespace and control characters (<= 0x20 or 0x7f). The netrc format is whitespace-delimited, so embedded spaces, tabs, or newlines could inject additional machine entries.
  • Automatic cleanup: The temporary credential directory is automatically removed after the git operation completes.
  • Terminal prompt disabled: GIT_TERMINAL_PROMPT=0 is set to prevent git from hanging on interactive credential prompts in automated environments.

github#

Interact with GitHub via GraphQL (reads, issues, PRs, review threads, signed commits, branches, tags, repos, branch protection) and REST (releases, CI check runs, workflow runs, labels, milestones, reactions, collaborators, webhooks, Actions workflows, variables, environments, repo settings, topics, custom properties, security settings). Uses the configured GitHub auth handler automatically. Commit operations use createCommitOnBranch for GPG-signed multi-file atomic commits. Includes a generic api_call operation for arbitrary GitHub REST endpoints.

For arbitrary GitHub REST endpoints not covered by a named operation, use the built-in api_call operation instead of falling back to the http provider.

Capabilities#

from, transform, action

Inputs#

FieldTypeRequiredDescription
operationstringYesAPI operation (see operations table below)
ownerstringConditionalRepository owner (user or organization). Required for all operations except create_repo (defaults to authenticated user).
repostringYesRepository name. Required for all operations.
api_basestringGitHub API base URL (default: https://api.github.com). Set for GitHub Enterprise.
pathstringFile path within the repository (for get_file)
refstringGit reference (branch, tag, or commit SHA). Defaults to repo’s default branch
numberintIssue or pull request number
statestringFilter by state: open, closed, all, merged (default: open)
per_pageintResults per page, 1–100 (default: 30)
titlestringTitle for issue or pull request create/update
bodystringBody text for issue, pull request, release, or comment
labelsarrayLabels to apply (names for issues)
assigneesarrayAssignee login usernames
headstringHead branch for creating a pull request
basestringBase branch for creating a pull request
draftboolCreate PR as draft
merge_methodstringMerge method: MERGE, SQUASH, REBASE (default: MERGE)
branchstringBranch name for commit/branch/tag operations
messagestringCommit message headline
expected_head_oidstringExpected HEAD OID (40-char SHA) for optimistic locking in create_commit
additionsarrayFiles to add/update: [{path, content}]
deletionsarrayFiles to delete: [{path}]
oidstringGit object ID (SHA) for branch/tag creation
tagstringTag name for tag operations
tag_namestringTag name for release
namestringRelease name/title
release_idintRelease ID for update/delete
prereleaseboolWhether release is a prerelease
target_commitishstringBranch/SHA target for release tag
visibilitystringRepository visibility for create_repo: public or private (default: public)
auto_initboolInitialize repo with a README (uses REST Contents API)
ruleset_namestringName for the repository ruleset
targetstringRuleset target type: branch or tag (default: branch)
enforcementstringRuleset enforcement level: active, disabled, evaluate (default: active)
include_refsarrayRef patterns to include (e.g. refs/heads/main, refs/tags/v*)
exclude_refsarrayRef patterns to exclude
required_status_checks_contextsarrayRequired status check context names
required_approving_review_countintMinimum approving reviews required (0–10)
required_linear_historyboolRequire linear commit history
allow_force_pushesboolAllow force pushes to matching refs
allow_deletionsboolAllow deletion of matching refs
requires_commit_signaturesboolRequire signed commits
commit_shastringCommit SHA for list_commit_pulls (alias: sha)
endpointstringRelative API path for api_call (e.g. /repos/{owner}/{repo}/labels). Must start with /
methodstringHTTP method for api_call: GET, POST, PUT, PATCH, DELETE (default: GET)
request_bodyobjectRequest body for api_call (used with POST/PUT/PATCH)
query_paramsobjectQuery string parameters for api_call (e.g. {per_page: 100})
state_reasonstringReason for closing an issue: completed, not_planned, reopened
thread_idstringReview thread node ID for reply_to_review_thread and resolve_review_thread
run_idintWorkflow run ID for get_workflow_run, cancel_workflow_run, rerun_workflow
commit_titlestringCommit title for merge_pull_request
commit_messagestringCommit message body for merge_pull_request
message_bodystringExtended commit message body for create_commit
descriptionstringDescription for repo, label, milestone, or environment
label_namestringLabel name for label CRUD operations
new_label_namestringNew label name when renaming (update_label)
colorstringHex color code for labels, without # (e.g. 00ff00)
label_descriptionstringDescription for a label
milestone_numberintMilestone number for update/delete operations
due_onstringDue date for milestone (ISO 8601: YYYY-MM-DDTHH:MM:SSZ)
reaction_contentstringReaction emoji: +1, -1, laugh, confused, heart, hooray, rocket, eyes
reaction_subjectstringSubject type for reaction: issue, pull_request, issue_comment (default: issue)
reaction_idintReaction ID for delete_reaction
comment_idintComment ID for reaction/comment operations
usernamestringGitHub username for collaborator operations
permissionstringPermission level: pull, triage, push, maintain, admin
webhook_urlstringPayload URL for the webhook
webhook_eventsarrayEvents that trigger the webhook (e.g. ["push", "pull_request"])
webhook_content_typestringContent type for webhook payloads: json or form (default: json)
webhook_secretstringSecret for webhook signature verification (sensitive)
webhook_activeboolWhether the webhook is active
hook_idintWebhook ID for update/delete operations
workflow_idstringWorkflow file name or numeric ID (e.g. ci.yml)
workflow_inputsobjectInput parameters for dispatch_workflow
workflow_statusstringFilter workflow runs by status (e.g. completed, in_progress, queued)
variable_namestringRepository variable name
variable_valuestringRepository variable value
environment_namestringDeployment environment name
wait_timerintWait timer in minutes before deployments proceed (0–43200)
reviewersarrayRequired reviewers for environment (objects with type and id)
homepagestringRepository homepage URL (update_repo)
default_branchstringDefault branch name (update_repo)
has_issuesboolEnable issues feature (update_repo)
has_projectsboolEnable projects feature (update_repo)
has_wikiboolEnable wiki feature (update_repo)
allow_squash_mergeboolAllow squash merging (update_repo)
allow_merge_commitboolAllow merge commits (update_repo)
allow_rebase_mergeboolAllow rebase merging (update_repo)
delete_branch_on_mergeboolAuto-delete head branches after merge (update_repo)
archivedboolArchive the repository (update_repo)
topicsarrayRepository topics/tags for replace_topics
organizationstringOrganization to fork into (fork_repo)
default_branch_onlyboolFork only the default branch (fork_repo)
new_repo_namestringName for new repository (create_from_template)
new_ownerstringOwner for new repository (create_from_template)
include_all_branchesboolInclude all branches when creating from template
propertiesobjectCustom properties to set (key-value map for set_custom_properties)

Operations#

Read operations (capabilities: from, transform):

OperationDescription
get_repoGet repository metadata
get_fileGet file content (returned as plain text, not base64)
list_releasesList releases
get_latest_releaseGet the latest release
list_pull_requestsList pull requests (filterable by state)
get_pull_requestGet a single pull request
list_issuesList issues (filterable by state)
get_issueGet a single issue
list_issue_commentsList comments on an issue
list_branchesList branches
get_branchGet a single branch
list_tagsList tags
get_head_oidGet HEAD commit SHA for a branch
list_pr_commentsList comments on a pull request
list_review_threadsList review threads on a pull request
list_check_runsList check runs for a ref (REST)
get_workflow_runGet a workflow run by ID (REST)
list_workflow_runsList workflow runs (REST, filterable by status)
list_commit_pullsList pull requests associated with a commit SHA (REST)
list_labelsList repository labels (REST)
list_milestonesList milestones (REST)
list_reactionsList reactions on an issue, PR, or comment (REST)
list_collaboratorsList repository collaborators (REST)
list_webhooksList repository webhooks (REST)
list_repo_variablesList repository variables (REST)
list_environmentsList deployment environments (REST)
list_topicsList repository topics (REST)
list_custom_propertiesList custom properties (REST)
api_callGeneric REST API call to any GitHub endpoint (see below)

Write operations (capability: action — returns success boolean):

OperationAPIDescription
create_issueGraphQLCreate a new issue
update_issueGraphQLUpdate an existing issue
create_issue_commentGraphQLAdd a comment to an issue
create_pull_requestGraphQLOpen a new pull request
update_pull_requestGraphQLUpdate a pull request
merge_pull_requestGraphQLMerge a pull request
close_pull_requestGraphQLClose a pull request
create_commitGraphQLCreate a GPG-signed commit (multi-file, atomic)
create_branchGraphQLCreate a branch
delete_branchGraphQLDelete a branch
create_tagGraphQLCreate a lightweight tag
delete_tagGraphQLDelete a tag
create_releaseRESTCreate a release
update_releaseRESTUpdate a release
delete_releaseRESTDelete a release
create_repoGraphQLCreate a repository (REST fallback for EMU)
create_rulesetRESTCreate a repository ruleset (branch or tag protection)
enable_vulnerability_alertsRESTEnable Dependabot vulnerability alerts
enable_automated_security_fixesRESTEnable Dependabot automated security fixes
update_repoRESTUpdate repository settings
reply_to_review_threadGraphQLReply to a review thread
resolve_review_threadGraphQLResolve a review thread
create_labelRESTCreate a repository label
update_labelRESTUpdate a label (name, color, description)
delete_labelRESTDelete a label
add_labels_to_issueRESTAdd labels to an issue
remove_label_from_issueRESTRemove a label from an issue
create_milestoneRESTCreate a milestone
update_milestoneRESTUpdate a milestone
delete_milestoneRESTDelete a milestone
add_reactionRESTAdd a reaction to an issue, PR, or comment
delete_reactionRESTRemove a reaction
add_collaboratorRESTAdd a repository collaborator
remove_collaboratorRESTRemove a repository collaborator
create_webhookRESTCreate a repository webhook
update_webhookRESTUpdate a webhook
delete_webhookRESTDelete a webhook
dispatch_workflowRESTTrigger a workflow dispatch event
cancel_workflow_runRESTCancel a workflow run
rerun_workflowRESTRe-run a workflow
create_or_update_variableRESTCreate or update a repository variable
delete_variableRESTDelete a repository variable
create_or_update_environmentRESTCreate or update a deployment environment
delete_environmentRESTDelete a deployment environment
replace_topicsRESTReplace all repository topics
fork_repoRESTFork a repository
create_from_templateRESTCreate a repository from a template
set_custom_propertiesRESTSet custom properties on a repository

api_call operation#

A generic escape hatch for any GitHub REST API endpoint not covered by a named operation. Keeps auth handling, GHE hostname resolution, and API versioning consistent without needing the http provider.

  • endpoint is required and must be a relative path starting with /
  • Path traversal (..) and query strings (?) in endpoint are rejected – use query_params instead
  • For GET requests, the output uses readOutput (same shape as other read operations)
  • For mutating methods (POST/PUT/PATCH/DELETE), the output uses actionOutput
  • api_call is intentionally excluded from the write-operation guard – the user controls the HTTP method, so it cannot be classified at the provider level

Output#

Read operations:

FieldTypeDescription
resultanyAPI response — structure varies by operation

Write operations (action):

FieldTypeDescription
successboolAlways true on success (failures return a Go error)
operationstringThe operation that was performed
resultanyAPI response data

Write operations return a Go error on failure, which stops dependent workflow actions and respects onError handling.

The create_commit result includes additions and deletions as integer counts (e.g., "additions": 3), not nested objects.

Examples#

# Get repository info
resolve:
  with:
    - provider: github
      inputs:
        operation: get_repo
        owner: octocat
        repo: hello-world

# Get file content from a specific branch (returns plain text)
transform:
  with:
    - provider: github
      inputs:
        operation: get_file
        owner: octocat
        repo: hello-world
        path: README.md
        ref: main

# Create a GPG-signed commit with multiple files
action:
  with:
    - provider: github
      inputs:
        operation: create_commit
        owner: my-org
        repo: my-repo
        branch: feature-branch
        message: "feat: add scaffolded files"
        expected_head_oid: abc123def456789012345678901234567890abcd
        additions:
          - path: src/main.go
            content: "package main\n\nfunc main() {}\n"
          - path: README.md
            content: "# My Project\n"

# Create an issue with labels
action:
  with:
    - provider: github
      inputs:
        operation: create_issue
        owner: my-org
        repo: my-repo
        title: "Bug: something is broken"
        body: "Steps to reproduce..."
        labels:
          - bug
          - priority/high

# Create a release (uses REST API)
action:
  with:
    - provider: github
      inputs:
        operation: create_release
        owner: my-org
        repo: my-repo
        tag_name: v1.0.0
        name: "Release 1.0.0"
        body: "First stable release"

# Create a repository with auto-init
action:
  with:
    - provider: github
      inputs:
        operation: create_repo
        owner: my-org
        repo: my-new-repo
        description: "A scaffolded project"
        visibility: private
        auto_init: true

# Create a branch protection ruleset
action:
  with:
    - provider: github
      inputs:
        operation: create_ruleset
        owner: my-org
        repo: my-repo
        ruleset_name: main branch protection
        target: branch
        enforcement: active
        include_refs:
          - refs/heads/main
        required_status_checks_contexts:
          - test
          - lint
        required_approving_review_count: 1
        required_linear_history: true
        requires_commit_signatures: true
        allow_force_pushes: false
        allow_deletions: false

# Enable Dependabot security features
action:
  with:
    - provider: github
      inputs:
        operation: enable_vulnerability_alerts
        owner: my-org
        repo: my-repo

# Update repository settings
action:
  with:
    - provider: github
      inputs:
        operation: update_repo
        owner: my-org
        repo: my-repo
        description: "Updated description"
        has_wiki: false
        delete_branch_on_merge: true

# Reply to a review thread
action:
  with:
    - provider: github
      inputs:
        operation: reply_to_review_thread
        owner: my-org
        repo: my-repo
        thread_id: "PRRT_abc123"
        body: "Fixed in the latest commit, thanks!"

# Create and manage labels
action:
  with:
    - provider: github
      inputs:
        operation: create_label
        owner: my-org
        repo: my-repo
        label_name: "priority/critical"
        color: "e11d48"
        label_description: "Critical priority issues"

# Add labels to an issue
action:
  with:
    - provider: github
      inputs:
        operation: add_labels_to_issue
        owner: my-org
        repo: my-repo
        number: 42
        labels:
          - bug
          - priority/critical

# Create a milestone
action:
  with:
    - provider: github
      inputs:
        operation: create_milestone
        owner: my-org
        repo: my-repo
        title: "v2.0.0"
        description: "Second major release"
        due_on: "2026-06-01T00:00:00Z"

# Add a reaction to an issue
action:
  with:
    - provider: github
      inputs:
        operation: add_reaction
        owner: my-org
        repo: my-repo
        number: 42
        reaction_content: "+1"

# Add a collaborator
action:
  with:
    - provider: github
      inputs:
        operation: add_collaborator
        owner: my-org
        repo: my-repo
        username: octocat
        permission: push

# Create a webhook
action:
  with:
    - provider: github
      inputs:
        operation: create_webhook
        owner: my-org
        repo: my-repo
        webhook_url: "https://example.com/webhook"
        webhook_events:
          - push
          - pull_request
        webhook_content_type: json

# Dispatch a workflow
action:
  with:
    - provider: github
      inputs:
        operation: dispatch_workflow
        owner: my-org
        repo: my-repo
        workflow_id: deploy.yml
        ref: main
        workflow_inputs:
          environment: production

# List workflow runs (read)
resolve:
  with:
    - provider: github
      inputs:
        operation: list_workflow_runs
        owner: my-org
        repo: my-repo
        workflow_id: ci.yml
        workflow_status: completed

# Create or update a repository variable
action:
  with:
    - provider: github
      inputs:
        operation: create_or_update_variable
        owner: my-org
        repo: my-repo
        variable_name: DEPLOY_ENV
        variable_value: production

# Create a deployment environment with reviewers
action:
  with:
    - provider: github
      inputs:
        operation: create_or_update_environment
        owner: my-org
        repo: my-repo
        environment_name: production
        wait_timer: 30
        reviewers:
          - type: User
            id: 12345

# Replace repository topics
action:
  with:
    - provider: github
      inputs:
        operation: replace_topics
        owner: my-org
        repo: my-repo
        topics:
          - go
          - cli
          - scaffolding

# Fork a repository
action:
  with:
    - provider: github
      inputs:
        operation: fork_repo
        owner: upstream-org
        repo: cool-project
        organization: my-org
        default_branch_only: true

# Create from template
action:
  with:
    - provider: github
      inputs:
        operation: create_from_template
        owner: template-org
        repo: go-template
        new_owner: my-org
        new_repo_name: my-new-service
        description: "Service from template"
        include_all_branches: false

# List check runs for a ref (read)
resolve:
  with:
    - provider: github
      inputs:
        operation: list_check_runs
        owner: my-org
        repo: my-repo
        ref: main

# List pull requests associated with a commit
resolve:
  with:
    - provider: github
      inputs:
        operation: list_commit_pulls
        owner: my-org
        repo: my-repo
        commit_sha: abc123def456

# Generic REST API call (read)
resolve:
  with:
    - provider: github
      inputs:
        operation: api_call
        endpoint: /repos/my-org/my-repo/labels
        query_params:
          per_page: 100

# Generic REST API call (write)
action:
  with:
    - provider: github
      inputs:
        operation: api_call
        endpoint: /repos/my-org/my-repo/labels
        method: POST
        request_body:
          name: bug
          color: d73a4a
          description: "Something isn't working"

For a complete end-to-end example of repo creation with rulesets and security settings, see the plugin-template solution .


go-template#

Transform data using Go text/template syntax. Supports single-template rendering (render, the default) and batch directory rendering (render-tree).

Capabilities#

transform, action

Inputs#

FieldTypeRequiredDescription
operationstringOperation: render (default) or render-tree
templatestringGo template content (required for render)
namestringTemplate name for error messages (defaults to "render-tree" for render-tree)
entriesarrayArray of {path, content, data?} objects to render (required for render-tree). Each entry may include an optional data map, shallow-merged over the shared data for that entry only.
missingKeystringBehavior for missing keys: default, zero, error
leftDelimstringLeft delimiter (default: {{)
rightDelimstringRight delimiter (default: }})
dataanyAdditional data to merge with resolver context
ignoredBlocksarrayExtra literal pass-through markers (on top of the always-on built-ins). Each entry uses EXACTLY ONE mode: { start, end } (multi-line, markers preserved), { line } (single-line), or { token } (every occurrence of a literal). Built-in zero-config markers {{/* scafctl:ignore:start */}}{{/* scafctl:ignore:end */}} (markers stripped) and # scafctl:ignore (per-line) need no declaration.

Output#

render operation: Returns the rendered template as a string.

render-tree operation: Returns an array of {path, content} objects where each content is the rendered result. Metadata includes templateName and entryCount.

Per-entry data (fan-out)#

Each entry may carry an optional data map that is shallow-merged over the shared top-level data for that entry only. On key conflicts, per-entry values win over shared data, iteration variables, and resolver context. This enables fan-out – one template rendered once per item, each with its own variables and output path – in a single resolver, without a separate forEach render step or a manual index-zip:

resolve:
  with:
    - provider: go-template
      inputs:
        operation: render-tree
        data:
          platformAppName: my-app        # shared default (merge base)
        entries:
          expr: |
            _.environments.map(env, {
              "path":    "envs/" + env.name + "/backend.tf",
              "content": _.backendTemplate.entries[0].content,
              "data":    {"environment": env}   # per-entry, wins over shared
            })

Entries without a data field render against the shared data alone, so existing render-tree usage is unaffected. A non-map data value fails with a clear error naming the entry index and path.

Ignored Blocks#

Use ignoredBlocks to bypass template rendering for specific sections. This is useful when templates contain syntax that conflicts with Go template delimiters (e.g., Terraform ${}, Helm {{ }}, GitHub Actions ${{ }}).

Start/End Mode (Multi-line)#

Define start and end markers. All content between matched markers (inclusive) is preserved:

transform:
  with:
    - provider: go-template
      inputs:
        name: terraform-config
        template: |
          resource "aws_instance" "main" {
            ami           = "{{ .ami }}"
            instance_type = "{{ .instanceType }}"
            /*scafctl:ignore:start*/
            tags = {
              Name = "${var.name}"
            }
            /*scafctl:ignore:end*/
          }
        ignoredBlocks:
          - start: "/*scafctl:ignore:start*/"
            end: "/*scafctl:ignore:end*/"

Line Mode (Single-line)#

Define a line marker. Every line containing that substring is preserved literally:

transform:
  with:
    - provider: go-template
      inputs:
        name: workflow-config
        template: |
          name: Deploy {{ .appName }}
          steps:
            - run: echo ${{ secrets.TOKEN }}  # scafctl:ignore
            - run: echo "deployed"
        ignoredBlocks:
          - line: "# scafctl:ignore"

Note: line and start/end are mutually exclusive within a single entry, but different entries can use different modes.

The content between start and end markers (including the markers themselves) passes through unchanged. For line mode, the entire line containing the marker is preserved.

Examples#

# Render a single template
transform:
  with:
    - provider: go-template
      inputs:
        name: config
        template: |
          server:
            host: {{ .host }}
            port: {{ .port }}
            env: {{ .environment }}

# Batch-render a directory of templates (render-tree)
resolve:
  with:
    - provider: go-template
      inputs:
        operation: render-tree
        entries:
          expr: '_.templateFiles.entries'
        data:
          rslvr: vars

hcl#

Process HCL (HashiCorp Configuration Language) content. Supports four operations: parse (default) extracts structured block information; format canonically formats; validate checks syntax; generate produces HCL from structured input. Accepts single files, multiple paths, or a directory of .tf files.

Capabilities#

from, transform

Inputs#

FieldTypeRequiredDescription
operationstringparse (default), format, validate, or generate
contentstringRaw HCL content to process
pathstringPath to a single HCL file
pathsarrayArray of HCL file paths (merged for parse; per-file for format/validate)
dirstringDirectory path — all .tf/.tf.json files are processed
blocksobjectStructured block data for generate (same schema as parse output)
output_formatstringGeneration output format: hcl (default) or json (Terraform JSON syntax .tf.json)

Source selection: For parse/format/validate, provide exactly one of content, path, paths, or dir (mutually exclusive). For generate, use blocks and optionally output_format.

Output — parse (default)#

FieldTypeDescription
variablesarrayVariable blocks (name, type, default, description, sensitive, validation)
resourcesarrayResource blocks (type, name, attributes, sub-blocks)
dataarrayData source blocks (type, name, attributes, sub-blocks)
modulesarrayModule blocks (name, source, version, attributes)
outputsarrayOutput blocks (name, value, description, sensitive)
localsmapLocal values merged across all locals blocks
providersarrayProvider configuration blocks (name, alias, attributes)
terraformobjectTerraform block (required_version, required_providers, backend, cloud)
movedarrayMoved blocks (from, to)
importarrayImport blocks (to, id, provider)
checkarrayCheck blocks (name, data, assertions)

When multiple files are parsed (paths or dir), results are merged: arrays are concatenated, locals and terraform maps are merged (last-file-wins for conflicting keys).

Output — format#

FieldTypeDescription
formattedstringThe canonically formatted HCL content (single file)
changedbooltrue if the formatter modified the content

Multi-file format returns { files: [{filename, formatted, changed}, ...], changed: bool }.

Output — validate#

FieldTypeDescription
validbooltrue if no syntax errors were found
error_countintNumber of error-level diagnostics
diagnosticsarrayDiagnostic entries with severity, summary, detail, range

Multi-file validate returns { valid: bool, error_count: int, files: [{filename, valid, error_count, diagnostics}, ...] }.

Output — generate#

FieldTypeDescription
hclstringGenerated HCL text (native HCL syntax or Terraform JSON depending on output_format)

Metadata includes output_format (hcl or json) indicating which format was produced.

Examples#

# Parse inline HCL content (operation defaults to "parse")
resolve:
  with:
    - provider: hcl
      inputs:
        content: |
          variable "region" {
            type    = string
            default = "us-east-1"
          }

# Parse an HCL file
resolve:
  with:
    - provider: hcl
      inputs:
        path: ./main.tf

# Parse all .tf files in a directory (results merged)
resolve:
  with:
    - provider: hcl
      inputs:
        dir: ./terraform

# Parse multiple specific files
resolve:
  with:
    - provider: hcl
      inputs:
        paths:
          - ./main.tf
          - ./variables.tf
          - ./outputs.tf

# Transform: parse HCL from another resolver's output
transform:
  with:
    - provider: hcl
      inputs:
        content: "{{ .resolvers.tfFile.content }}"

# Format inline HCL content
resolve:
  with:
    - provider: hcl
      inputs:
        operation: format
        content: |
          variable "region" {
          type=string
          default="us-east-1"
          }

# Format all files in a directory
resolve:
  with:
    - provider: hcl
      inputs:
        operation: format
        dir: ./terraform

# Validate HCL syntax
resolve:
  with:
    - provider: hcl
      inputs:
        operation: validate
        path: ./main.tf

# Generate HCL from structured data
resolve:
  with:
    - provider: hcl
      inputs:
        operation: generate
        blocks:
          variables:
            - name: region
              type: string
              default: us-east-1
              description: "AWS region"

# Generate Terraform JSON (.tf.json) from structured data
resolve:
  with:
    - provider: hcl
      inputs:
        operation: generate
        output_format: json
        blocks:
          variables:
            - name: region
              type: string
              default: us-east-1
          resources:
            - type: aws_instance
              name: web
              attributes:
                ami: ami-12345
                instance_type: t3.micro

http#

HTTP client for API calls with built-in pagination support for fetching data across multiple pages.

Capabilities#

from, transform, action

Inputs#

FieldTypeRequiredDescription
urlstringURL to request
methodstringHTTP method (default: GET)
headersobjectHTTP headers
bodyanyRequest body. A string is sent verbatim; an object or array is serialized to compact JSON and Content-Type defaults to application/json when not otherwise set
timeoutintTimeout in seconds (max 300)
retryobjectRetry configuration
authstringAuth provider (e.g., entra, github)
scopestringOAuth scope for authentication
paginationobjectPagination configuration (see below)
autoParseJsonboolParse response body as JSON when Content-Type is application/json. Enables direct field access (e.g., _.result.body.items)
pollobjectPolling configuration — re-execute request until a condition is met (see below)

Request body#

body accepts either a string or a structured value:

  • String – sent verbatim. You control the exact bytes and the Content-Type header.
  • Object or array – serialized to compact JSON. When you do not set a Content-Type header, the provider defaults it to application/json. An explicit Content-Type (matched case-insensitively) is always preserved.

Passing a structured body lets you build the payload directly from resolver values with a CEL expression, avoiding a separate serializer resolver:

# Object body built from a CEL expression -- serialized to JSON automatically
provider: http
inputs:
  url: https://api.example.com/graphql
  method: POST
  body:
    expr: '{"query": _.graphQuery, "variables": {"id": _.recordId}}'

Pagination#

The pagination input enables automatic multi-page fetching. Five strategies are supported to cover different API pagination patterns.

Pagination Fields#

FieldTypeRequiredDescription
strategystringOne of: offset, pageNumber, cursor, linkHeader, custom
maxPagesintSafety limit for max pages to fetch (default: 100, max: 10000)
collectPathstringCEL expression to extract items from each response (e.g., body.items)
stopWhenstringCEL expression; if true, stop paginating (e.g., size(body.items) == 0)

CEL variables available in collectPath, stopWhen, and strategy-specific expressions:

VariableTypeDescription
statusCodeintHTTP response status code
bodyanyParsed JSON response body
rawBodystringRaw response body string
headersobjectResponse headers
pageintCurrent page number (1-based)

Strategy: offset#

Increments an offset query parameter each page.

FieldTypeDefaultDescription
limitint(required)Page size
offsetParamstringoffsetQuery parameter name for offset
limitParamstringlimitQuery parameter name for limit

Strategy: pageNumber#

Increments a page number query parameter each page.

FieldTypeDefaultDescription
pageSizeint(required)Page size
pageParamstringpageQuery parameter name for page number
pageSizeParamstringpageSizeQuery parameter name for page size
startPageint1Starting page number

Strategy: cursor#

Extracts a cursor token or next URL from the response to fetch subsequent pages.

FieldTypeDescription
nextTokenPathstringCEL expression to extract cursor from response (e.g., body.nextCursor)
nextTokenParamstringQuery parameter to set with the cursor value (required with nextTokenPath unless bodyTemplate is set)
nextURLPathstringCEL expression to extract the full next page URL (e.g., body['@odata.nextLink']). Alternative to nextTokenPath.

Use nextTokenPath + nextTokenParam for APIs that return a token. Use nextURLPath for APIs that return a full URL (e.g., Microsoft Graph @odata.nextLink).

Strategy: linkHeader#

Follows rel="next" links in the Link response header (RFC 8288). Used by GitHub, GitLab, and other REST APIs. No additional configuration needed.

Strategy: custom#

Full control using CEL expressions.

FieldTypeDescription
nextURLstringCEL expression returning the full next page URL (empty string = stop)
nextParamsstringCEL expression returning a map of query params for the next request (empty map = stop)

Body-Based Pagination (bodyTemplate)#

For APIs that paginate via the request body (GraphQL, Elasticsearch), use bodyTemplate to generate the POST body for each page instead of modifying URL query parameters.

FieldTypeDescription
bodyTemplatestringCEL expression evaluated per-page to produce the request body. Overrides the top-level body field. The expression may return a string (sent as-is) or a map/list (automatically JSON-marshaled). If the expression evaluates to null, an empty body is sent.

CEL variables available in bodyTemplate:

VariableTypeDescription
__pageintCurrent page number (starts at startPage, default 1)
__pageSizeintPage size from pageSize (pageNumber/cursor/linkHeader/custom) or limit (offset)
__offsetintCurrent offset (increments by __pageSize each page)
__cursorstringLast cursor value from nextTokenPath (cursor strategy)

When bodyTemplate is set, the URL remains unchanged across all pages — pagination state is carried entirely in the request body.

# GraphQL pagination with bodyTemplate
provider: http
inputs:
  url: https://api.example.com/graphql
  method: POST
  headers:
    Content-Type: application/json
  pagination:
    strategy: cursor
    maxPages: 10
    pageSize: 50
    nextTokenPath: "body.data.pageInfo.endCursor"
    bodyTemplate: |
      __cursor == "" ?
        '{"query":"{ items(first: ' + string(__pageSize) + ') { nodes { id } pageInfo { endCursor hasNextPage } } }"}'
      :
        '{"query":"{ items(first: ' + string(__pageSize) + ', after: \"' + __cursor + '\") { nodes { id } pageInfo { endCursor hasNextPage } } }"}'
    collectPath: "body.data.items.nodes"
    stopWhen: "body.data.items.pageInfo.hasNextPage == false"

# Elasticsearch scroll with bodyTemplate
provider: http
inputs:
  url: https://es.example.com/my-index/_search
  method: POST
  headers:
    Content-Type: application/json
  pagination:
    strategy: offset
    maxPages: 20
    limit: 100
    bodyTemplate: |
      '{"from": ' + string(__offset) + ', "size": ' + string(__pageSize) + ', "query": {"match_all": {}}}'
    collectPath: "body.hits.hits"
    stopWhen: "size(body.hits.hits) == 0"

Polling#

The poll input enables re-executing the request until a response condition is met. This is different from retry (which handles transient failures) — polling re-executes on successful responses until the content matches expectations.

FieldTypeRequiredDescription
untilstringCEL expression evaluated against the response. Polling stops when this returns true. Available variables: body (parsed if JSON), statusCode, headers
failWhenstringCEL expression that triggers immediate failure (e.g., terminal error states)
intervalstringDuration between polls (default: 5s). Format: 1s, 30s, 2m
maxAttemptsintMaximum number of poll attempts (default: 60)
# Wait for deployment to complete
resolve:
  with:
    - provider: http
      inputs:
        url: https://api.example.com/deployments/123/status
        method: GET
        auth: entra
        autoParseJson: true
        poll:
          until: 'body.status == "succeeded"'
          failWhen: 'body.status == "failed"'
          interval: 10s
          maxAttempts: 30

Output#

FieldTypeDescription
statusCodeintHTTP status code (last page when paginating)
bodyanyResponse body as string, or parsed JSON object when autoParseJson: true. When paginating with collectPath, contains JSON array of all collected items
headersobjectResponse headers (last page when paginating)
successboolWhether request succeeded (action only)
pagesintNumber of pages fetched (only when paginating)
totalItemsintTotal items collected across all pages (only when paginating)

Non-JSON and Empty Response Bodies#

body is only parsed into a structured value when autoParseJson is enabled and the response Content-Type is JSON and the body is non-empty. Otherwise body is the raw response string, and an empty response yields an empty string ("").

Some endpoints return a bare scalar as text/plain rather than JSON — for example, a Microsoft Graph $count endpoint returns a number like 42, and may return an empty body when there is nothing to count. Coercing such a body directly to a number fails on the empty string, so guard it in CEL and supply a default before converting:

resolvers:
  user_count_raw:
    resolve:
      with:
        - provider: http
          inputs:
            url: https://graph.microsoft.com/v1.0/users/$count
            headers:
              ConsistencyLevel: eventual

  user_count:
    type: int
    dependsOn: [user_count_raw]
    resolve:
      with:
        - provider: cel
          inputs:
            # Treat an empty/whitespace body as 0, then convert to int.
            expression: int(_.user_count_raw.body.trim() == "" ? "0" : _.user_count_raw.body.trim())

This keeps the generic http provider free of endpoint-specific behavior while making the numeric coercion explicit and safe at the solution level.

Examples#

# GET request
resolve:
  with:
    - provider: http
      inputs:
        url: https://api.example.com/config
        headers:
          Accept: application/json

# POST with body
provider: http
inputs:
  url: https://api.example.com/deploy
  method: POST
  headers:
    Content-Type: application/json
  body:
    expr: 'toJson({"image": _.image, "env": _.environment})'
  timeout: 60

# With retry
provider: http
inputs:
  url: https://api.example.com/status
  retry:
    maxAttempts: 3
    backoff: exponential
    initialDelay: 1s

# Authenticated GitHub API request
resolve:
  with:
    - provider: http
      inputs:
        url: https://api.github.com/user/repos
        headers:
          Accept: application/json
        auth: github
        scope: repo

# Cursor pagination (token-based)
provider: http
inputs:
  url: https://api.example.com/items
  pagination:
    strategy: cursor
    maxPages: 10
    nextTokenPath: "body.nextCursor"
    nextTokenParam: "cursor"
    collectPath: "body.items"
    stopWhen: "body.nextCursor == null"

# Cursor pagination (OData / Microsoft Graph nextLink)
provider: http
inputs:
  url: https://graph.microsoft.com/v1.0/users?$top=100
  authProvider: entra
  scope: "https://graph.microsoft.com/.default"
  pagination:
    strategy: cursor
    maxPages: 50
    nextURLPath: "body['@odata.nextLink']"
    collectPath: "body.value"

# Link header pagination (GitHub-style)
provider: http
inputs:
  url: https://api.github.com/users/octocat/repos?per_page=30
  headers:
    Accept: application/vnd.github+json
  pagination:
    strategy: linkHeader
    maxPages: 5
    collectPath: "body"

# Offset pagination
provider: http
inputs:
  url: https://api.example.com/records
  pagination:
    strategy: offset
    maxPages: 20
    limit: 50
    collectPath: "body.records"
    stopWhen: "size(body.records) < 50"

# Page number pagination
provider: http
inputs:
  url: https://api.example.com/products
  pagination:
    strategy: pageNumber
    maxPages: 10
    pageSize: 25
    pageParam: "page"
    pageSizeParam: "per_page"
    collectPath: "body.products"
    stopWhen: "size(body.products) == 0"

# Custom pagination with CEL expressions
provider: http
inputs:
  url: https://api.example.com/search?q=test
  pagination:
    strategy: custom
    maxPages: 10
    nextURL: "has(body.links) && has(body.links.next) ? body.links.next : ''"
    collectPath: "body.results"
    stopWhen: "!has(body.links) || !has(body.links.next)"

Security#

The HTTP provider enforces several security measures:

  • SSRF protection: Requests to private, loopback, and link-local IP addresses (e.g., 169.254.169.254) are blocked by default. Set httpClient.allowPrivateIPs: true in config to allow private network access for on-premises endpoints.
  • Response body size limit: Each response is limited to httpClient.maxResponseBodySize (default: 100 MB). This prevents denial-of-service via unbounded responses from malicious or misconfigured servers. Applies to both direct requests and each page in paginated requests.
  • Redirect validation: Each redirect target is checked against the SSRF private IP blocklist. A maximum of 10 redirects is enforced.
  • Pagination host validation: Pagination next URLs must stay on the same hostname as the original request to prevent open redirect attacks.
  • Token security: Authentication tokens are injected via the Authorization header and are never logged. Token refresh on 401 responses is handled transparently.
# Override the default response body size limit (e.g. for large API responses)
# In config.yaml:
httpClient:
  maxResponseBodySize: 209715200  # 200 MB

identity#

Get authentication identity information without exposing tokens. Supports reading stored session metadata or minting a fresh scoped access token to inspect its claims on demand.

Capabilities#

from

Inputs#

FieldTypeRequiredDescription
operationstringOperation: status, claims, groups, list
handlerstringAuth handler name (e.g., entra, github)
scopestringOAuth scope for on-demand token minting. When set, claims and status mint a fresh access token for the scope and return its details instead of stored session metadata. Not supported for groups or list.

Operations#

OperationDescription
statusReturns authentication status, expiry, and identity type from stored session metadata (or scoped token when scope is set)
claimsReturns identity claims (name, email, tenant, etc.) from stored session metadata (or scoped token JWT when scope is set)
groupsReturns Entra group memberships for the authenticated user
listLists all registered auth handler names

Output#

FieldTypeDescription
operationstringThe operation that was executed
handlerstringThe auth handler that was used
authenticatedboolWhether authenticated
identityTypestringIdentity type: user or service-principal
claimsobjectToken claims (email, name, subject, tenantId, etc.)
tenantIdstringTenant ID (for Entra)
expiresAtstringToken expiration in RFC3339 format
expiresInstringHuman-readable duration until expiry
groupsarrayGroup display names (for groups operation)
handlersarrayAvailable handler names (for list operation)
scopedTokenbooltrue when the response was derived from a scoped access token
tokenScopestringThe OAuth scope the token was minted for (when scope input was set)
tokenTypestringToken type, typically Bearer (when scope input was set)
flowstringAuth flow that produced the token (when scope input was set)
sessionIdstringStable session identifier (when scope input was set)

Opaque tokens: When scope is provided and the access token is not a decodable JWT (e.g., encrypted Microsoft Graph tokens), claims will be null and a warning is added to the output. Token metadata (expiry, type) is still returned where available.

Examples#

# Check if authenticated (Entra)
resolve:
  with:
    - provider: identity
      inputs:
        operation: status
        handler: entra

# Check if authenticated (GitHub)
resolve:
  with:
    - provider: identity
      inputs:
        operation: status
        handler: github

# Get claims from stored session metadata
resolve:
  with:
    - provider: identity
      inputs:
        operation: claims
        handler: entra

# Get GitHub claims (login, name, email)
resolve:
  with:
    - provider: identity
      inputs:
        operation: claims
        handler: github

# Mint a scoped token and inspect its claims
resolve:
  with:
    - provider: identity
      inputs:
        operation: claims
        scope: api://my-app/.default

# Check scoped token status (expiry, flow, tokenType)
resolve:
  with:
    - provider: identity
      inputs:
        operation: status
        scope: https://management.azure.com/.default
        handler: entra

# List all registered auth handlers
resolve:
  with:
    - provider: identity
      inputs:
        operation: list

# Get Entra group memberships
resolve:
  with:
    - provider: identity
      inputs:
        operation: groups
        handler: entra

metadata#

Returns runtime metadata about the scafctl process and the currently-executing solution, including platform information. Requires no inputs – all data is gathered from the execution context and process environment. Useful for conditional logic, auditing, cross-platform solutions, and passing runtime info to templates and downstream resolvers.

Capabilities#

from

Inputs#

None. The metadata provider accepts no inputs.

Output#

FieldTypeDescription
versionobjectBuild version information
version.buildVersionstringSemantic version of the scafctl build
version.commitstringGit commit hash of the build
version.buildTimestringTimestamp of the build
argsstring[]Command-line arguments passed to scafctl
cwdstringCurrent working directory
entrypointstringHow scafctl was invoked: "cli", "api", or "unknown"
commandstringThe command path (e.g. scafctl/run/solution)
solutionobjectMetadata about the currently-running solution
solution.namestringSolution name
solution.versionstringSolution version
solution.displayNamestringSolution display name
solution.descriptionstringSolution description
solution.categorystringSolution category
solution.tagsstring[]Solution tags
osstringOperating system (runtime.GOOS): darwin, linux, windows, etc.
archstringCPU architecture (runtime.GOARCH): amd64, arm64, etc.
shellstringDetected shell name (e.g. bash, zsh, fish, pwsh, powershell, cmd.exe). On Unix, reads $SHELL. On Windows, detects PowerShell via PSModulePath and parent process inspection, then falls back to Git Bash ($SHELL) and finally %ComSpec%. Empty if not detected.

Examples#

Resolve runtime metadata:

resolvers:
  runtime-meta:
    resolve:
      with:
        - provider: metadata

Use metadata in a downstream resolver via CEL:

resolvers:
  runtime-meta:
    resolve:
      with:
        - provider: metadata
  greeting:
    dependsOn: [runtime-meta]
    resolve:
      with:
        - provider: cel
          inputs:
            expression: >-
              "Running " + _.runtime_meta.solution.name +
              " v" + _.runtime_meta.solution.version +
              " via " + _.runtime_meta.entrypoint

message#

Outputs styled terminal messages with built-in types, custom formatting via lipgloss, destination control, and respects --quiet and --no-color flags. For dynamic interpolation, use the framework’s tmpl: or expr: ValueRef on the message input — the provider does not handle templating internally. Use type: raw for machine-readable output that bypasses the 8192-character limit and writes content directly without formatting.

Capabilities#

action

Inputs#

FieldTypeRequiredDescription
messagestringMessage text to output. Use tmpl: or expr: ValueRef for dynamic interpolation. Limited to 8192 characters unless type: raw.
typestringMessage type: success, warning, error, info (default), debug, plain, raw
labelstringContextual prefix rendered as dimmed [label] between icon and message (e.g., step 2/5). Not supported with type: raw.
styleobjectCustom formatting that merges on top of type defaults: color (hex or named), bold, italic, icon. Not supported with type: raw.
destinationstringOutput target: stdout (default) or stderr
newlineboolAppend trailing newline (default: true)

Output#

FieldTypeDescription
successboolAlways true on success
messagestringRendered message text

Examples#

Built-in type styling:

resolvers:
  step1:
    resolve:
      with:
        - provider: message
          inputs:
            message: "Build succeeded"
            type: success

Custom style with icon:

resolvers:
  deploy:
    resolve:
      with:
        - provider: message
          inputs:
            message: "Starting pipeline"
            style:
              color: "#FF5733"
              bold: true
              icon: "\U0001F680"

Go template interpolation via tmpl: ValueRef:

resolvers:
  config:
    resolve:
      with:
        - provider: static
          inputs:
            value:
              appName: my-service
              version: 2.0.0
  deploy-msg:
    resolve:
      with:
        - provider: message
          inputs:
            message:
              tmpl: "Deploying {{ .config.appName }} v{{ .config.version }}"
            type: info

CEL expression via expr: ValueRef:

resolvers:
  items:
    resolve:
      with:
        - provider: static
          inputs:
            value: [a, b, c]
  status:
    resolve:
      with:
        - provider: message
          inputs:
            message:
              expr: "'Processed ' + string(size(_.items)) + ' items'"
            type: success

Raw output (machine-readable, no length limit):

actions:
  emit-index:
    steps:
      - provider: message
        inputs:
          message:
            rslvr: registry_index_json
          type: raw
          newline: false

parameter#

Access CLI parameters passed via -r flags.

Capabilities#

from

Inputs#

FieldTypeRequiredDescription
keystringParameter name (exact match). Provide either key or keys; key takes precedence over keys when both are set.
keysarray of stringOrdered list of parameter names (aliases) for a single logical parameter. The first name provided via CLI wins. Evaluated after key.
defaultanyValue returned when the parameter is not provided.
typestringHow the value is coerced. One of auto (default), string, raw, int, float, bool, json, csv, fetch. See the table below.

At least one of key or keys must be provided.

type values#

ValueBehavior
autoDefault. Infers booleans, numbers, JSON, and file:// sources, falling back to the literal string. http:///https:// values are not fetched (they stay literal strings – use fetch), and comma-separated values are not split into a list (opt in with csv).
stringCoerces the value to a string, stripping surrounding quotes. Use to keep a numeric-looking value (leading zeros, or a value used with CEL matches()) as a string.
rawReturns the value untouched – no coercion or quote-stripping. A numeric YAML default stays numeric; a CLI string stays verbatim. The escape hatch to disable inference.
intForces integer parsing. A non-integer value is an error.
floatForces floating-point parsing. A non-numeric value is an error.
boolForces boolean parsing, accepting only true/false (case-insensitive). Any other value is an error.
jsonParses a string value as JSON. Invalid JSON is an error; non-string values pass through unchanged.
csvSplits a comma-separated string into a list of trimmed strings. Non-string values pass through unchanged.
fetchPerforms an SSRF-guarded HTTP GET on an http:///https:// value and returns the response body. A non-URL value is an error. For anything beyond a plain GET (auth, headers, methods, retries), use the http provider.

Output#

FieldTypeDescription
valueanyParameter value
foundboolWhether parameter was provided
typestringDetected type of value

Examples#

# Get parameter with fallback
resolve:
  with:
    - provider: parameter
      inputs:
        key: environment
    - provider: static
      inputs:
        value: "dev"  # Default if not provided
# Accept the same parameter under any of several flag names (aliases).
# Whichever of -r environment / -r e / -r env was provided wins.
resolve:
  with:
    - provider: parameter
      inputs:
        keys: [environment, e, env]
        default: dev
# Coerce values with the "type" input. "auto" (default) infers bools,
# numbers, JSON, and file:// sources; http(s):// URLs stay literal strings
# (use "fetch" to GET them); other types force one coercion.
resolve:
  with:
    # Keep a numeric-looking ID as a string (leading zeros survive).
    - provider: parameter
      inputs:
        key: billingId
        type: string
        default: "00042"
    # Force integer parsing (a non-integer value is an error).
    - provider: parameter
      inputs:
        key: port
        type: int
        default: 8080
    # Opt in to list splitting: "a,b,c" -> [a, b, c].
    - provider: parameter
      inputs:
        key: regions
        type: csv
        default: us-east-1
    # Return the value exactly as received (no coercion, no quote-stripping).
    - provider: parameter
      inputs:
        key: token
        type: raw

Usage:

scafctl run solution -f sol.yaml -r environment=production
scafctl run solution -f sol.yaml -r environment=production

secret#

Retrieve encrypted secrets from the scafctl secrets store.

Capabilities#

from

Inputs#

FieldTypeRequiredDescription
operationstringOperation: get or list
namestringSecret name (for get)
patternstringRegex pattern to match names
requiredboolError if not found
defaultstringValue when not found

Examples#

# Get secret
resolve:
  with:
    - provider: secret
      inputs:
        operation: get
        name: api-key
        required: true

# Get with default
resolve:
  with:
    - provider: secret
      inputs:
        operation: get
        name: optional-key
        default: "fallback-value"

Manage secrets via CLI:

scafctl secrets set api-key "my-secret-value"
scafctl secrets list
scafctl secrets set api-key "my-secret-value"
scafctl secrets list

sleep#

Pause execution for a specified duration.

Capabilities#

from, transform, validation, action

Inputs#

FieldTypeRequiredDescription
durationstringDuration (Go format: 1s, 500ms, 2m)

Examples#

# Wait between API calls
provider: sleep
inputs:
  duration: "2s"

solution#

Execute a sub-solution and return its results as a structured envelope. Supports recursive composition with circular reference detection and configurable depth limits. In from mode, only the resolver phase runs. In action mode, resolvers run first, then the workflow (if present).

Capabilities#

from, action

Inputs#

FieldTypeRequiredDescription
sourcestringYesSub-solution location: file path, catalog reference (e.g. deploy-to-k8s@2.0.0), or URL
inputsobjectParameters passed to the sub-solution’s parameter provider
resolversarrayResolver names to execute from the child solution; when empty all resolvers run
propagateErrorsboolWhether sub-solution failures cause a Go error (default: true)
maxDepthintMaximum nesting depth for recursive composition, 1–100 (default: 10)
timeoutstringMaximum duration for sub-solution execution (Go duration, e.g. 30s, 5m)

Output#

From capability (resolver-only):

FieldTypeDescription
resolversobjectResolver values from the sub-solution (keyed by resolver name)
statusstringOverall status: success or failed
errorsarrayResolver errors encountered during execution

Action capability (resolvers + workflow):

FieldTypeDescription
resolversobjectResolver values from the sub-solution
workflowobjectAggregate workflow status (finalStatus, failedActions, skippedActions)
statusstringOverall status: success or failed
errorsarrayResolver errors encountered during execution
successboolWhether the solution succeeded

Examples#

# Load child solution (from capability)
resolve:
  with:
    - provider: solution
      inputs:
        source: "./child-solution.yaml"
        inputs:
          environment: "production"

# Execute child solution with workflow (action capability)
action:
  with:
    - provider: solution
      inputs:
        source: "deploy-to-k8s@2.0.0"
        inputs:
          cluster: prod-east
          replicas: 3
        timeout: "5m"

# Select specific resolvers from child solution
resolve:
  with:
    - provider: solution
      inputs:
        source: "./shared/config.yaml"
        resolvers:
          - database_config
          - cache_config

# Soft failures (don't propagate errors)
resolve:
  with:
    - provider: solution
      inputs:
        source: "./optional-enrichment.yaml"
        propagateErrors: false

# Load-tolerant composition: if the child YAML is missing or malformed,
# the parent continues with a degraded envelope (status: "failed",
# errors: [{resolver: "_loader", message: "..."}]) instead of aborting.
resolve:
  with:
    - provider: solution
      inputs:
        source: "./optional-child.yaml"
        propagateErrors: false

state#

Read a resolver value that was persisted on a previous run. The value comes from the state snapshot loaded at the start of the run – i.e. the value from the prior run, before the current run overwrites it at save time. Pair with a resolver marked persist: true (or immutable: true) to reproduce the legacy “read another resolver’s previously saved value” pattern.

The key input is treated as an opaque string: it never creates a dependency edge in the resolver graph, so a resolver may safely read its own prior value without forming a self-cycle.

Capabilities#

from

Inputs#

FieldTypeRequiredDescription
operationstringState operation. Only get is supported (the default).
keystringName of the persisted resolver whose prior-run value to read.
defaultanyValue returned when the key has no persisted entry (e.g. on the first run). Returns null when omitted.

Examples#

resolvers:
  # Recomputes fresh every run and records its value for next time.
  db_password:
    persist: true
    resolve:
      with:
        - provider: exec
          inputs:
            command: "openssl rand -hex 16"

  # Reads the value db_password persisted on the PRIOR run.
  prior_password:
    resolve:
      with:
        - provider: state
          inputs:
            operation: get
            key: db_password
            default: ""

Within a single run, state.get("db_password") returns the prior run’s value while _.db_password returns the value produced by the current run.


static#

Return a constant value. Useful for defaults and fallbacks.

Capabilities#

from, transform

Inputs#

FieldTypeRequiredDescription
valueanyStatic value to return

Examples#

# Default fallback
resolve:
  with:
    - provider: env
      inputs:
        operation: get
        name: CONFIG_PATH
    - provider: static
      inputs:
        value: "/etc/app/config.yaml"

# Complex default
resolve:
  with:
    - provider: static
      inputs:
        value:
          timeout: 30
          retries: 3
          endpoints:
            - https://primary.example.com
            - https://backup.example.com

validation#

Validate data using regex patterns and CEL expressions.

Capabilities#

transform, validation

Inputs#

FieldTypeRequiredDescription
valuestringValue to validate (uses __self in transform context)
matchstringRegex pattern that must match
notMatchstringRegex pattern that must NOT match
expressionstringCEL expression that must be true
messagestringCustom error message on failure

Output#

FieldTypeDescription
validboolWhether validation passed
errorsarrayValidation error messages
detailsstringFailure details

Examples#

# Validate with regex
validate:
  with:
    - provider: validation
      inputs:
        match: "^[a-z][a-z0-9-]+$"
        message: "Name must be lowercase alphanumeric with dashes"

# Validate with CEL
validate:
  with:
    - provider: validation
      inputs:
        expression: "__self in ['dev', 'staging', 'prod']"
        message: "Environment must be dev, staging, or prod"

# Combined validation
validate:
  with:
    - provider: validation
      inputs:
        match: "^v[0-9]+\\.[0-9]+\\.[0-9]+$"
        expression: "!__self.startsWith('v0.')"
        message: "Version must be semver format and >= v1.0.0"

forEach with Providers#

The forEach clause iterates over an array, executing a provider once per element and collecting results into an output array.

Supported Phases#

PhaseSupportedforEach.inNotes
transform.withYesOptional (defaults to __self)Most common – transform an existing value
resolve.withYesRequiredNo __self in resolve phase
validate.withNoNot supported

forEach Clause Fields#

FieldTypeRequiredDescription
itemstringNoVariable name alias for current array element. __item is always available.
indexstringNoVariable name alias for current 0-based index. __index is always available.
inValueRefResolve: Yes, Transform: NoArray to iterate over. Defaults to __self (transform only).
concurrencyintNoMaximum parallel iterations. 0 (default) means unlimited.
keepSkippedboolNoRetain nil entries for items skipped by when condition (default: false).
onErrorstringNoError handling: fail (default) or continue. Actions only; resolvers ignore this.

How It Works#

  1. The in source (or __self for transform) is evaluated to get an input array.
  2. The provider executes once per element, with __item/__index (and any custom aliases) injected into the expression context.
  3. Results are collected into an output array preserving order.
  4. If a when condition is present, items where when is false are removed from the output (unless keepSkipped: true).

Examples#

# Resolve: fan-out HTTP requests directly (no static placeholder needed)
resolve:
  with:
    - provider: http
      forEach:
        in:
          rslvr: moduleList
        item: mod
        concurrency: 10
      inputs:
        url:
          expr: '"https://registry.example.com/v1/modules/" + mod.name + "/versions"'
        method: GET
        autoParseJson: true

# Transform: double each number in an array
transform:
  with:
    - provider: cel
      forEach:
        item: num
        index: i
      inputs:
        expression: "num * 2"

# Transform: filter and keep only active users
transform:
  with:
    - provider: cel
      forEach:
        item: user
      when:
        expr: "user.active == true"
      inputs:
        expression: "user"

# Preserve index alignment with keepSkipped
transform:
  with:
    - provider: cel
      forEach:
        item: x
        keepSkipped: true
      when:
        expr: "x > 5"
      inputs:
        expression: "x"

On resolve steps, forEach.in is required because there is no __self in the resolve phase. On transform steps, forEach.in defaults to __self (the current value being transformed).

For a full walkthrough with runnable examples, see the Resolver Tutorial – Array Iteration with forEach .


Next Steps#