DOC_INDEX
THEME
DOCS/Example Cookbook/Templates & Expressions Examples

Templates & Expressions Examples

OrchStep examples for Go templates, JavaScript, and data transforms


data transform

Data Transform Function

# Example: Data Transform Function
# The `transform` function executes JavaScript to process,
# filter, and reshape data within your workflow.
#
# Usage:
#   func: transform
#   do: |
#     // JavaScript code
#     return { key: value };
#
# Available context:
#   vars.*            - Read/write workflow variables
#   steps.*           - Read outputs from previous steps
#   utils.parseJSON() - Parse JSON strings
#   utils.stringifyJSON() - Serialize to JSON
#   utils.unique()    - Deduplicate an array
#   utils.sum()       - Sum an array of numbers
#
# Return value becomes the step output:
#   - Object: fields accessible as steps.<name>.<field>
#   - Primitive/Array: wrapped as steps.<name>.result
#
# Try: orchstep run filter-api-data
# Try: orchstep run compute-metrics

name: data-transform-demo
desc: "JavaScript transform function for data manipulation"

tasks:
  # -- Return different types --
  basic-transforms:
    desc: "Transform returning primitives, objects, and arrays"
    steps:
      - name: return_number
        func: transform
        do: |
          return 42;
      # Access: steps.return_number.result == 42

      - name: return_string
        func: transform
        do: |
          return "deployment-ready";
      # Access: steps.return_string.result == "deployment-ready"

      - name: return_object
        func: transform
        do: |
          return {
            count: 42,
            status: "success",
            active: true
          };
      # Access: steps.return_object.count, steps.return_object.status

      - name: return_array
        func: transform
        do: |
          return [1, 2, 3, 4, 5];
      # Access: steps.return_array.result.length == 5

      - name: verify
        func: shell
        do: |
          echo "Number: {{ steps.return_number.result }}"
          echo "Object status: {{ steps.return_object.status }}"

  # -- Access and modify variables --
  compute-metrics:
    desc: "Read variables and compute derived values"
    vars:
      input_value: 100
      multiplier: 2
      counter: 0
    steps:
      - name: calculate
        func: transform
        do: |
          const input = vars.input_value;
          const mult = vars.multiplier;
          return { result: input * mult };

      - name: increment_counter
        func: transform
        do: |
          // Transform can modify task-level variables
          vars.counter = vars.counter + 1;
          return { incremented: true };

      - name: read_counter
        func: transform
        do: |
          return { counter_value: vars.counter };

      - name: show_results
        func: shell
        do: |
          echo "Computed: {{ steps.calculate.result }}"
          echo "Counter: {{ steps.read_counter.counter_value }}"

  # -- Filter and reshape API data --
  filter-api-data:
    desc: "Parse API response and filter active users"
    steps:
      - name: fetch_users
        func: shell
        do: |
          echo '{"users":[{"id":1,"name":"Alice","active":true},{"id":2,"name":"Bob","active":false},{"id":3,"name":"Charlie","active":true}]}'

      - name: process_users
        func: transform
        do: |
          const data = utils.parseJSON(steps.fetch_users.output);
          const activeUsers = data.users.filter(u => u.active);
          const names = activeUsers.map(u => u.name);

          return {
            total_users: data.users.length,
            active_count: activeUsers.length,
            active_names: names
          };

      - name: display_results
        func: shell
        do: |
          echo "Total users: {{ steps.process_users.total_users }}"
          echo "Active users: {{ steps.process_users.active_count }}"

  # -- Utility functions --
  utility-functions:
    desc: "Use built-in utility functions for common operations"
    steps:
      - name: json_round_trip
        func: transform
        do: |
          // Serialize and parse JSON
          const obj = { name: "deployment", version: 42 };
          const json = utils.stringifyJSON(obj);
          const parsed = utils.parseJSON(json);
          return { name: parsed.name, version: parsed.version };

      - name: array_operations
        func: transform
        do: |
          // Deduplicate and sum
          const arr = [1, 2, 2, 3, 3, 3];
          const unique = utils.unique(arr);
          const total = utils.sum(unique);
          return {
            original_count: arr.length,
            unique_count: unique.length,
            sum: total
          };

      - name: show_results
        func: shell
        do: |
          echo "JSON name: {{ steps.json_round_trip.name }}"
          echo "Unique values: {{ steps.array_operations.unique_count }}"
          echo "Sum: {{ steps.array_operations.sum }}"

dual syntax

Go Templates vs JavaScript Expressions

# Example: Go Templates vs JavaScript Expressions
# OrchStep auto-detects which syntax you are using:
#
#   - Starts with {{ }}  ->  Go Template (Sprig functions)
#   - Otherwise          ->  JavaScript (Goja engine)
#
# Both syntaxes work in assert conditions, retry when clauses,
# and anywhere expressions are evaluated.
#
# Try: orchstep run compare-syntax

name: dual-syntax-demo
desc: "Side-by-side comparison of Go template and JavaScript"

defaults:
  environment: "prod"
  count: 10
  status: "ok"
  message: "deployment successful"
  items:
    - name: "web-server"
      cpu: 100
    - name: "database"
      cpu: 200

tasks:
  # -- Same assertions in both syntaxes --
  compare-syntax:
    desc: "Equivalent checks in Go template and JavaScript"
    steps:
      # === Equality ===
      - name: go_equality
        desc: "Go Template: equality check"
        func: assert
        args:
          condition: "{{ eq vars.environment \"prod\" }}"
          desc: "Environment is prod (Go template)"

      - name: js_equality
        desc: "JavaScript: equality check"
        func: assert
        args:
          condition: "vars.environment === 'prod'"
          desc: "Environment is prod (JavaScript)"

      # === Numeric comparison ===
      - name: go_numeric
        desc: "Go Template: numeric comparison"
        func: assert
        args:
          condition: "{{ gt vars.count 5 }}"
          desc: "Count > 5 (Go template)"

      - name: js_numeric
        desc: "JavaScript: numeric comparison"
        func: assert
        args:
          condition: "vars.count > 5"
          desc: "Count > 5 (JavaScript)"

      # === String contains ===
      - name: go_contains
        desc: "Go Template: string contains"
        func: assert
        args:
          condition: "{{ contains \"successful\" vars.message }}"
          desc: "Message contains 'successful' (Go template)"

      - name: js_contains
        desc: "JavaScript: string contains"
        func: assert
        args:
          condition: "vars.message.includes('successful')"
          desc: "Message contains 'successful' (JavaScript)"

      # === Logical AND ===
      - name: go_and
        desc: "Go Template: logical AND"
        func: assert
        args:
          condition: "{{ and (eq vars.status \"ok\") (gt vars.count 5) }}"
          desc: "Status ok AND count > 5 (Go template)"

      - name: js_and
        desc: "JavaScript: logical AND"
        func: assert
        args:
          condition: "vars.status === 'ok' && vars.count > 5"
          desc: "Status ok AND count > 5 (JavaScript)"

      # === Logical OR ===
      - name: go_or
        desc: "Go Template: logical OR"
        func: assert
        args:
          condition: "{{ or (eq vars.environment \"prod\") (eq vars.environment \"staging\") }}"
          desc: "Environment is prod or staging (Go template)"

      - name: js_or
        desc: "JavaScript: logical OR"
        func: assert
        args:
          condition: "vars.environment === 'prod' || vars.environment === 'staging'"
          desc: "Environment is prod or staging (JavaScript)"

      # === NOT / Negation ===
      - name: go_not
        desc: "Go Template: NOT"
        func: assert
        args:
          condition: "{{ not (eq vars.environment \"dev\") }}"
          desc: "Environment is not dev (Go template)"

      - name: js_not
        desc: "JavaScript: NOT"
        func: assert
        args:
          condition: "vars.environment !== 'dev'"
          desc: "Environment is not dev (JavaScript)"

  # -- JavaScript-only features --
  javascript-extras:
    desc: "JavaScript supports array access and complex expressions"
    steps:
      - name: array_access
        desc: "Access array elements by index"
        func: assert
        args:
          condition: "vars.items[0].cpu === 100"
          desc: "First item CPU is 100"

      - name: array_length
        desc: "Check array length"
        func: assert
        args:
          condition: "vars.items.length === 2"
          desc: "Must have exactly 2 items"

      - name: complex_expression
        desc: "Multi-line JavaScript expression"
        func: assert
        args:
          condition: |
            vars.count > 5 &&
            vars.status === 'ok' &&
            vars.message.includes('successful')
          desc: "All conditions must be met"

  # -- Quick reference --
  syntax-summary:
    desc: "Print a quick reference of both syntaxes"
    steps:
      - name: show_reference
        func: shell
        do: |
          echo "=== Syntax Quick Reference ==="
          echo ""
          echo "Go Template:"
          echo '  Equality:     {{ eq vars.x "value" }}'
          echo '  Greater than: {{ gt vars.n 5 }}'
          echo '  Contains:     {{ contains "text" vars.s }}'
          echo '  AND:          {{ and (cond1) (cond2) }}'
          echo '  OR:           {{ or (cond1) (cond2) }}'
          echo '  NOT:          {{ not (cond) }}'
          echo ""
          echo "JavaScript:"
          echo "  Equality:     vars.x === 'value'"
          echo "  Greater than: vars.n > 5"
          echo "  Contains:     vars.s.includes('text')"
          echo "  AND:          cond1 && cond2"
          echo "  OR:           cond1 || cond2"
          echo "  NOT:          !cond  or  vars.x !== 'value'"
          echo ""
          echo "Detection: starts with {{ -> Go Template, otherwise -> JavaScript"

go templates

Go Template Syntax

# Example: Go Template Syntax
# OrchStep supports Go templates with Sprig functions for
# string manipulation, conditionals, and data formatting.
#
# Syntax: {{ expression }}
#
# Key features:
#   - Variable access: {{ vars.name }}, {{ .vars.name }} (legacy dot syntax)
#   - Step outputs: {{ steps.step_name.output }}
#   - Environment: {{ env.PATH }}, {{ .env.HOME }}
#   - Sprig functions: upper, lower, trim, contains, default, etc.
#   - Conditionals: {{ if }}, {{ else }}, {{ end }}
#   - Comparisons: eq, ne, gt, lt, and, or, not
#
# Try: orchstep run string-operations
# Try: orchstep run conditional-config

name: go-templates-demo
desc: "Go template syntax with Sprig functions"

defaults:
  app_name: "web-api"
  environment: "production"
  version: "2.5.0"

tasks:
  # -- String manipulation with Sprig --
  string-operations:
    desc: "Use Sprig functions for string operations"
    steps:
      - name: format_strings
        func: shell
        do: |
          echo "App: {{ vars.app_name }}"
          echo "Uppercase: {{ upper vars.app_name }}"
          echo "Environment: {{ upper vars.environment }}"
          echo "Deployment ID: {{ vars.app_name }}-{{ vars.environment }}"

      - name: use_default_values
        func: shell
        do: |
          # default provides a fallback when a value is empty
          echo "Region: {{ vars.region | default "us-east-1" }}"
          echo "Timeout: {{ vars.timeout | default "30s" }}"

  # -- Conditionals in templates --
  conditional-config:
    desc: "Use if/else for environment-specific configuration"
    steps:
      - name: configure_for_env
        func: shell
        do: |
          echo "=== Configuration for {{ vars.environment }} ==="
          echo "Log level: {{ if eq vars.environment "production" }}error{{ else }}debug{{ end }}"
          echo "Replicas: {{ if eq vars.environment "production" }}3{{ else }}1{{ end }}"
          echo "Debug: {{ if ne vars.environment "production" }}true{{ else }}false{{ end }}"

  # -- Variable interpolation patterns --
  variable-patterns:
    desc: "Different ways to access variables in templates"
    vars:
      deployment_id: "{{ vars.app_name }}-{{ vars.environment }}"
      config_path: "/etc/{{ vars.app_name }}/config.yml"
    steps:
      - name: setup_environment
        func: shell
        do: |
          echo "Setting up {{ vars.app_name }} version {{ vars.version }}"
          echo "Config: {{ vars.config_path }}"
          echo "Deployment: {{ vars.deployment_id }}"
        outputs:
          status: "ready"
          app_url: "https://{{ vars.app_name }}.{{ vars.environment }}.example.com"

      - name: deploy_application
        func: shell
        do: |
          echo "Deploying to {{ steps.setup_environment.app_url }}"
          echo "Status: {{ steps.setup_environment.status }}"

  # -- Legacy dot syntax (still supported) --
  legacy-dot-syntax:
    desc: "The .vars, .env, .steps prefix syntax still works"
    steps:
      - name: legacy_example
        func: shell
        do: |
          # Both forms are equivalent:
          echo "New syntax: {{ vars.app_name }}"
          echo "Dot syntax: {{ .vars.app_name }}"
          echo "Env access: {{ .env.HOME }}"
        outputs:
          # Check if an env var exists
          has_home: "{{ if .env.HOME }}true{{ else }}false{{ end }}"

  # -- Assertions with Go template syntax --
  template-assertions:
    desc: "Use Go templates in assert conditions"
    steps:
      - name: check_equality
        func: assert
        args:
          condition: "{{ eq vars.environment \"production\" }}"
          desc: "Environment must be production"

      - name: check_comparison
        func: assert
        args:
          condition: "{{ and (eq vars.app_name \"web-api\") (ne vars.environment \"dev\") }}"
          desc: "Must be web-api and not dev"

      - name: check_contains
        func: assert
        args:
          condition: "{{ contains \"2.5\" vars.version }}"
          desc: "Version must include 2.5"

template files

External Template File Inclusion

# Example: External Template File Inclusion
# Use `templateFile` to load and render external template files
# within your workflow. Great for generating Kubernetes manifests,
# config files, or any structured output.
#
# Syntax: {{ templateFile "path/to/template.tpl" }}
#
# Template files:
#   - Are relative to the workflow directory
#   - Have full access to vars, steps, env context
#   - Support Sprig functions (upper, lower, default, etc.)
#   - Can include other templates (nested inclusion)
#   - Path traversal (..) is blocked for security
#
# This example includes template files in the templates/ subdirectory.
# See: templates/greeting.tpl, templates/k8s-deployment.tpl,
#      templates/k8s-metadata.tpl
#
# Try: orchstep run generate-greeting
# Try: orchstep run generate-k8s-manifest

name: template-files-demo
desc: "External template file inclusion for config generation"

defaults:
  name: "Alice"
  app_name: "payment-service"
  environment: "production"
  namespace: "prod-ns"
  replicas: 3
  image: "myregistry.io/payment-service"
  tag: "v2.1.0"

tasks:
  # -- Basic template file loading --
  generate-greeting:
    desc: "Render a simple template file with variables"
    steps:
      - name: render_greeting
        func: shell
        do: |
          mkdir -p output
          cat > output/greeting.txt <<'EOF'
          {{ templateFile "templates/greeting.tpl" }}
          EOF
          cat output/greeting.txt
        outputs:
          greeting: "{{ result.output }}"

      - name: show_result
        func: shell
        do: |
          echo "Generated greeting:"
          echo "{{ steps.render_greeting.greeting }}"

  # -- Nested template inclusion (Kubernetes manifests) --
  generate-k8s-manifest:
    desc: "Generate a K8s deployment manifest with nested templates"
    steps:
      - name: render_deployment
        func: shell
        do: |
          mkdir -p output
          cat > output/deployment.yaml <<'EOF'
          {{ templateFile "templates/k8s-deployment.tpl" }}
          EOF
          cat output/deployment.yaml
        outputs:
          manifest: "{{ result.output }}"

      - name: show_manifest
        func: shell
        do: |
          echo "=== Generated Kubernetes Manifest ==="
          echo "{{ steps.render_deployment.manifest }}"

  # -- Template with step context --
  generate-build-info:
    desc: "Use step outputs inside templates"
    steps:
      - name: get_build_id
        func: shell
        do: echo "build-$(date +%Y%m%d)-42"
        outputs:
          build_id: "{{ result.output | trim }}"

      - name: create_build_info
        func: shell
        do: |
          mkdir -p output
          cat > output/build-info.txt <<'EOF'
          Build ID: {{ steps.get_build_id.build_id }}
          App: {{ vars.app_name }}
          Image: {{ vars.image }}:{{ vars.tag }}
          Environment: {{ upper vars.environment }}
          EOF
          cat output/build-info.txt
        outputs:
          info: "{{ result.output }}"

  # -- Load template into a variable --
  template-to-variable:
    desc: "Render a template file and store in a variable"
    steps:
      - name: load_template
        func: shell
        do: echo "Template loaded"
        outputs:
          # Render the template and capture the output
          rendered: '{{ templateFile "templates/greeting.tpl" }}'

      - name: use_rendered
        func: shell
        do: |
          echo "Rendered template content:"
          echo "{{ steps.load_template.rendered }}"

  # -- Cleanup --
  cleanup:
    desc: "Remove generated output files"
    steps:
      - name: remove_output
        func: shell
        do: |
          rm -rf output
          echo "Output files removed"