Template Variables

    Create dynamic content in your StructKit configurations with template variables.

    Custom Delimiters

    StructKit uses custom Jinja2 delimiters to avoid conflicts with YAML:

    • Variables: {{@ and @}}
    • Blocks: {%@ and @%}
    • Comments: {#@ and @#}

    Basic Syntax

    Variable syntaxyaml
    files:
      - README.md:
          content: |
            # {{@ project_name @}}
            Welcome to {{@ project_name @}}!

    Block Syntax

    Conditionalsyaml
    files:
      - config.yaml:
          content: |
            {%@ if environment == "production" @%}
            debug: false
            {%@ else @%}
            debug: true
            {%@ endif @%}

    Interactive Variables

    Define variables that prompt users for input with descriptions, types, defaults, and validation:

    Variable definitionsyaml
    variables:
      - project_name:
          description: "The name of your project"
          type: string
          default: "MyProject"
      - port:
          description: "Application port"
          type: integer
          default: 8080
      - IS_ENABLED:
          type: boolean
          required: true
      - ENV:
          type: string
          enum: [dev, prod]
      - TOKEN:
          type: string
          env: MY_TOKEN

    Supported types: string, integer, number, boolean. Validation options include required, enum, regex/pattern, min/max, and env/default_from_env.

    Built-in Variables

    • file_name — The name of the file being processed
    • file_directory — The directory containing the file

    Custom Filters & Globals

    uuid()

    id: {{@ uuid() @}}

    now()

    generated_at: {{@ now() @}}

    Warning: uuid() and now() are non-deterministic. A file using either produces different content on every run, so it always shows up in structkit generate --dry-run --diff output — which destroys that diff's value as a drift check. Confine them to files marked skip_if_exists: true.

    env(name, default="")

    TOKEN={{@ env("TOKEN", "changeme") @}}

    read_file(path)

    {{@ read_file("INTRO.md") @}}

    current_repo()

    Returns owner/repo for the git repository in the working directory, read from remote.origin.url. Works with both HTTPS and SSH remotes and returns an error string for non-GitHub remotes. This is what makes README badge URLs work without declaring a variable.

    ![CI](https://github.com/{{@ current_repo() @}}/actions/workflows/ci.yml/badge.svg)

    to_yaml / from_yaml / to_json / from_json

    to_json accepts an optional indent argument.

    {{@ some_dict | to_yaml @}}
    {{@ some_dict | to_json @}}
    {{@ some_dict | to_json(indent=2) @}}

    latest_release(repo_name, strip_v=false, return_sha=false)

    Resolves the latest GitHub release of a repository. Both options are optional and appended, so existing one-argument calls behave exactly as before.

    ParameterTypeDefaultDescription
    repo_namestrrequiredGitHub repository in owner/name form
    strip_vboolfalseIn version mode, removes exactly one leading lowercase v from the tag. v1.2.3 → 1.2.3, vv1.2.3 → v1.2.3, V1.2.3 → V1.2.3
    return_shaboolfalseReturns the commit SHA identified by the release tag instead of the tag name
    latest_release optionsyaml
    files:
      - release-info.txt:
          content: |
            Version: {{@ "nodejs/node" | latest_release @}}                       # v22.0.0
            Version without v: {{@ "nodejs/node" | latest_release(strip_v=true) @}} # 22.0.0
            Commit: {{@ "nodejs/node" | latest_release(return_sha=true) @}}        # full commit SHA
    • When both return_sha=true and strip_v=true are set, return_sha wins and strip_v is a no-op (a SHA is never stripped).
    • SHA resolution: the SHA comes from the Git tag reference. Lightweight tags resolve directly; annotated tags are peeled until a commit is reached. Resolution examines at most 10 Git objects, so no more than nine annotated-tag hops may precede the final commit. The returned object ID must be a full 40-character SHA-1 or 64-character SHA-256 hex value.
    • Fallback: if repository lookup succeeds but the latest-release lookup fails, version mode returns the repository's default branch name and SHA mode returns that branch's head commit SHA. strip_v is never applied to a fallback branch name.
    • Errors: if repository lookup or fallback resolution fails, the filter returns LATEST_RELEASE_ERROR. Once a release has been selected, a tag that is missing, malformed, cyclic, too deeply nested, or does not resolve to a commit also returns LATEST_RELEASE_ERROR rather than silently falling back to an unrelated revision.
    • Requirements: the filter uses PyGithub — set the GITHUB_TOKEN environment variable for private repositories and for authenticated API rate limits.

    Handy for pinning Terraform providers:

    version = "{{@ "hashicorp/terraform-provider-aws" | latest_release(strip_v=true) @}}"
    source_sha = "{{@ "DataDog/terraform-provider-datadog" | latest_release(return_sha=true) @}}"

    slugify

    Takes no arguments. It lowercases the value, converts runs of whitespace to a single hyphen, then removes every character that is not a-z, 0-9, or -. Underscores are removed, not converted — My_Project becomes myproject.

    server_name {{@ project_name | slugify @}};

    For a different separator, chain replace:

    src/{{@ project_name | slugify | replace("-", "_") @}}/__init__.py

    default_branch

    branches: [ {{@ "httpdss/structkit" | default_branch @}} ]

    The with Clause

    Pass additional variables to nested structures:

    with clauseyaml
    folders:
      - frontend/:
          struct: project/react
          with:
            app_name: "{{@ project_name @}}-frontend"
            port: 3000
      - backend/:
          struct: project/node
          with:
            app_name: "{{@ project_name @}}-backend"
            port: 8000

    Advanced Examples

    Conditional File Creation

    files:
      - docker-compose.yml:
          skip: "{{@ not use_docker @}}"
          content: |
            version: '3.8'
            services:
              app:
                image: {{@ project_name | slugify @}}:latest

    Dynamic File Names

    files:
      - "src/{{@ module_name @}}/index.js":
          content: |
            // {{@ module_name @}} module
            export default {};

    Environment-Specific Content

    files:
      - config/{{@ environment @}}.yml:
          content: |
            {%@ if environment == "production" @%}
            database_url: {{@ production_db_url @}}
            {%@ else @%}
            database_url: sqlite:///dev.db
            {%@ endif @%}