Creating Molds
Molds are reusable YAML templates for creating repositories, scaffolding projects, and provisioning infrastructure through a self-service UI. You define the inputs, steps, and actions once; anyone on your team can run them from the portal without touching the command line.
Where molds live
Section titled “Where molds live”Place mold files in your repository under .shoehorn/molds/:
your-repo/ .shoehorn/ molds/ create-empty-github-repo.yaml scaffold-go-service.yaml provision-production-service.yamlShoehorn’s crawler discovers these files automatically. Once indexed, they appear in the Forge section of the portal.
Mold structure
Section titled “Mold structure”Every mold has the same top-level sections:
version: "1.0.0"
metadata: name: my-mold # unique identifier displayName: My Mold # shown in the UI description: What this mold does # shown on the mold card author: platform-team icon: server # icon name (not emoji) category: repository # repository, service, infrastructure tags: - golang - microservice
inputs: type: object required: - name - owner properties: name: type: string title: Service Name description: Name of the service to create
defaults: goVersion: "1.24" private: true
actions: - action: scaffold.go.service label: Scaffold Go Service description: Creates a new Go HTTP service primary: true
steps: - id: create-repo name: Create GitHub repository action: github.repo.create inputs: name: "${{ parameters.name }}" owner: "${{ parameters.owner }}"
output: links: - title: View Repository url: "${{ steps.create-repo.output.html_url }}"Two execution formats
Section titled “Two execution formats”Shoehorn supports two step formats in the same YAML structure. The engine detects which one you are using automatically.
Forge action steps (recommended for GitHub operations)
Section titled “Forge action steps (recommended for GitHub operations)”Use action: on each step. This is the simpler format — best when your mold creates repos, files, topics, or pull requests on GitHub.
steps: - id: create-repo name: Create GitHub repository action: github.repo.create inputs: name: "${{ parameters.name }}" owner: "${{ parameters.owner }}" private: true
- id: create-readme name: Add README action: github.file.create inputs: owner: "${{ parameters.owner }}" repo: "${{ parameters.name }}" path: README.md message: "docs: add README" content: | # ${{ parameters.name }} ${{ parameters.description }}Forge action steps run sequentially from top to bottom. Each step can reference outputs from previous steps via ${{ steps.step-id.output.field }}.
Conditional steps with if:
Section titled “Conditional steps with if:”Skip a step based on an input value or previous step output:
steps: - id: create-repo name: Create repository action: github.repo.create inputs: name: "${{ parameters.name }}" owner: "${{ parameters.owner }}"
- id: set-topics name: Set topics action: github.topics.set if: "${{ parameters.enableTopics }}" inputs: owner: "${{ parameters.owner }}" repo: "${{ parameters.name }}" topics: [golang, shoehorn]The step is skipped when if: resolves to "false" or an empty string. Any other value (including "true") means the step runs. Since ${{ }} preserves types, a boolean input works directly.
Common Forge actions:
| Action | What it does | Idempotent |
|---|---|---|
github.repo.create | Create a GitHub repository | Yes — returns existing repo if it already exists |
github.repo.update | Update repository settings | Yes |
github.file.create | Create or update a file in a repo | Yes — updates if file exists (uses SHA) |
github.template.apply | Apply a folder of templates into a repo | Yes — updates files if they exist |
github.topics.set | Set topics/tags on a repository | Yes |
github.pr.create | Create a pull request | Yes — returns existing PR if one is open |
github.team.add | Add a GitHub team to a repository | Yes — updates permission if already added |
github.collaborator.add | Add an individual collaborator | Yes — updates permission if already added |
catalog.entity.register | Register an entity in the Shoehorn catalog | Yes — upserts if entity exists |
For the full list of inputs, outputs, and examples for each action, see Forge Actions. The reference is split by action type so you can jump directly to GitHub repositories, files and templates, access, catalog registration, or catalog-backed selectors.
Forge action steps use the ${{ }} expression syntax:
${{ parameters.name }}or${{ inputs.name }}— access user-provided inputs${{ steps.create-repo.output.html_url }}— access output from a previous step${{ context.createdBy }}— access run/user metadata (see Context variables below)${{ secrets.KEY }}— access secrets (blocked in sandboxed mode)${{ env.KEY }}— access environment variables (blocked in sandboxed mode)
Workflow steps (for multi-system orchestration)
Section titled “Workflow steps (for multi-system orchestration)”Use adapter: on each step. This format supports HTTP calls, catalog lookups, and logging — useful when you need to coordinate across systems beyond GitHub.
steps: - id: fetch-team name: Fetch team details adapter: catalog config: operation: get_team id: "{{ .inputs.team_id }}"
- id: create-repo name: Create GitHub repository adapter: http depends_on: - fetch-team config: url: "https://api.github.com/orgs/{{ .inputs.owner }}/repos" method: POST headers: Accept: "application/vnd.github+json" Authorization: "Bearer {{ .secrets.GITHUB_TOKEN }}" body: name: "{{ .inputs.repo_name }}" description: "Repository for team {{ .inputs.team_id }}" private: true
- id: summary name: Log result adapter: log depends_on: - create-repo config: message: "Created repository {{ .inputs.repo_name }}"Available workflow adapters:
| Adapter | What it does | Key config fields |
|---|---|---|
http | Make HTTP requests to any API | url, method, headers, body, auth |
catalog | Query the Shoehorn catalog (teams, entities) | operation, id, filters |
log | Write a log message (useful for debugging) | message |
Workflow steps use Go template syntax: {{ .inputs.name }}, {{ .steps.id.outputs.field }}, {{ .secrets.KEY }}.
Key differences between the two:
| Forge action steps | Workflow steps | |
|---|---|---|
| Step field | action: | adapter: |
| Expression syntax | ${{ parameters.x }} | {{ .inputs.x }} |
| Best for | GitHub operations | Multi-system orchestration |
| Execution order | Sequential (top to bottom) | Explicit (depends_on:) |
| Conditional execution | if: field | condition: field |
You can tell which format a mold uses by looking at the steps — if they have action:, they use Forge action steps; if they have adapter:, they use workflow steps.
Context variables
Section titled “Context variables”The context namespace provides run and user metadata that is injected automatically — you don’t need to define these as inputs.
| Variable | Description |
|---|---|
${{ context.createdBy }} | Email of the user who triggered the run |
${{ context.userEmail }} | Same as createdBy (user email from JWT) |
${{ context.userId }} | User ID from the identity provider |
${{ context.tenantId }} | Tenant/organization ID |
${{ context.runId }} | Unique ID of this execution |
${{ context.moldSlug }} | Slug of the mold being executed |
Context variables work in both regular and sandboxed mode. For details and examples, see Expressions and Context.
Input schema
Section titled “Input schema”Inputs use JSON Schema. The UI renders a form automatically based on your schema.
Supported types
Section titled “Supported types”| Type | UI element | Example |
|---|---|---|
string | Text input | Service name, description |
string + enum | Dropdown select | Go version, environment |
string + pattern | Text input with validation | ^[a-z0-9-]+$ |
boolean | Checkbox | Private repo, enable monitoring |
integer / number | Number input | Port, replica count |
Validation options
Section titled “Validation options”inputs: type: object required: - name - owner properties: name: type: string title: Service Name description: Lowercase with hyphens only minLength: 1 maxLength: 100 pattern: "^[a-zA-Z0-9._-]+$" ui:options: placeholder: my-service
environment: type: string title: Target Environment enum: - staging - production
replicas: type: integer title: Replica Count minimum: 1 maximum: 10
enableMonitoring: type: boolean title: Enable MonitoringConditional fields with x-dependency
Section titled “Conditional fields with x-dependency”Show or hide fields based on other field values:
properties: applicationSet: type: boolean title: Enable ApplicationSet default: true
notifications: type: boolean title: Enable Notifications x-dependency: condition: applicationSet == true
git_provider: type: string title: Git Provider enum: [GitHub, GitLab] x-dependency: condition: applicationSet == true && notifications == true
gitlab_group: type: number title: GitLab Group ID x-dependency: condition: applicationSet == true && notifications == true && git_provider == 'GitLab'Conditions support ==, !=, &&, ||, and != null.
Defaults
Section titled “Defaults”Provide default values so users don’t need to fill in every field:
defaults: goVersion: "1.24" replicas: 2 enableMonitoring: true private: true environment: productionApproval workflows
Section titled “Approval workflows”For sensitive operations, require approval before execution:
approvalFlow: required: true auto_approve_after: 86400 # auto-approve after 24h if no response steps: - name: Security Review description: Security team must approve production provisioning approvers: - admin@company.com required_count: 1 - name: Platform Review description: Platform team must approve resource allocation approvers: - platform-lead@company.com required_count: 1When a user runs a mold with approval flow, the run enters pending_approval status. Approvers see the request in the Approvals tab and can approve or reject with comments. See Approval Workflows for details.
Output links
Section titled “Output links”After a mold runs, show the user where to find what was created:
output: links: - title: View Repository url: "${{ steps.create-repo.output.html_url }}"For workflow steps, use the outputs: section:
outputs: team: value: "{{ .steps.fetch-team.outputs.team }}" repo: value: "{{ .steps.create-repo.outputs.body }}"Examples
Section titled “Examples”Minimal: create an empty repository
Section titled “Minimal: create an empty repository”version: "1.0.0"
metadata: name: create-empty-github-repo displayName: Create Empty GitHub Repo description: Creates a new empty GitHub repository author: shoehorn icon: git-branch category: repository tags: - github - repository
inputs: type: object required: - name - owner properties: name: type: string title: Repository Name owner: type: string title: Owner private: type: boolean title: Private Repository default: true
actions: - action: github.repo.create label: Create Repository primary: true
steps: - id: create-repo name: Create GitHub Repository action: github.repo.create inputs: name: "${{ inputs.name }}" owner: "${{ inputs.owner }}" private: "${{ inputs.private }}"
output: links: - title: Repository url: "${{ steps.create-repo.output.html_url }}"Intermediate: scaffold a Go HTTP service
Section titled “Intermediate: scaffold a Go HTTP service”Creates a repo with main.go, Dockerfile, Makefile, .gitignore, and sets topics.
version: "1.0.0"
metadata: name: scaffold-go-service displayName: Scaffold Go HTTP Service description: Create a GitHub repository with a Go HTTP service scaffold author: shoehorn icon: server category: service tags: - golang - http - microservice
inputs: type: object required: - name - description - owner properties: name: type: string title: Service Name pattern: "^[a-zA-Z0-9._-]+$" ui:options: placeholder: my-http-service description: type: string title: Description owner: type: string title: GitHub Organization goVersion: type: string title: Go Version enum: ["1.23", "1.24", "1.25"] port: type: string title: HTTP Port pattern: "^[0-9]+$" private: type: boolean title: Private Repository
defaults: goVersion: "1.23" port: "8080" private: true
actions: - action: scaffold.go.service label: Scaffold Go Service primary: true
steps: - id: create-repo action: github.repo.create inputs: owner: "${{ parameters.owner }}" name: "${{ parameters.name }}" description: "${{ parameters.description }}" private: "${{ parameters.private }}"
- id: add-main action: github.file.create inputs: owner: "${{ parameters.owner }}" repo: "${{ parameters.name }}" path: main.go content: | package main
import ( "log" "net/http" )
func main() { log.Printf("Starting ${{ parameters.name }} on :${{ parameters.port }}") log.Fatal(http.ListenAndServe(":${{ parameters.port }}", nil)) }
- id: add-dockerfile action: github.file.create inputs: owner: "${{ parameters.owner }}" repo: "${{ parameters.name }}" path: Dockerfile content: | FROM golang:${{ parameters.goVersion }}-alpine AS builder WORKDIR /app COPY . . RUN CGO_ENABLED=0 go build -o /app/${{ parameters.name }} .
FROM alpine:3.19 COPY --from=builder /app/${{ parameters.name }} /app/ EXPOSE ${{ parameters.port }} ENTRYPOINT ["/app/${{ parameters.name }}"]
- id: set-topics action: github.topics.set inputs: owner: "${{ parameters.owner }}" repo: "${{ parameters.name }}" topics: [golang, http-service, microservice, shoehorn]
output: links: - title: View Repository url: "${{ steps.create-repo.output.html_url }}"Advanced: provision a production service with approval
Section titled “Advanced: provision a production service with approval”Creates a full production setup (repo, Go service, Dockerfile, K8s manifests, CI pipeline, monitoring) and requires security + platform team approval.
Key features demonstrated:
- Multi-step approval flow (security review, then platform review)
- 10+ Forge action steps (repo, main.go, go.mod, Dockerfile, K8s deployment, K8s service, CI pipeline, .gitignore, topics)
ui:optionswith placeholders for better form UX- Pattern validation on service names
- Integer inputs with min/max bounds
- Defaults for optional fields
For a worked end-to-end example, see provision-production-service.yaml in the demo repository’s .shoehorn/molds/ directory.
Advanced: workflow adapter mold (multi-system)
Section titled “Advanced: workflow adapter mold (multi-system)”Fetches team details from the catalog, then creates a GitHub repo with team context.
version: "2.0.0"
metadata: name: create-repo-for-team displayName: Create Repository for Team description: Fetch team details from the catalog and create a repo with team context author: shoehorn icon: users category: repository tags: - github - teams - catalog
inputs: type: object required: - team_id - repo_name - owner properties: team_id: type: string title: Team ID or Slug repo_name: type: string title: Repository Name owner: type: string title: GitHub Owner
actions: - action: repo.create.team label: Create Team Repository primary: true
steps: - id: fetch-team name: Fetch Team Details adapter: catalog config: operation: get_team id: "{{ .inputs.team_id }}"
- id: create-repo name: Create GitHub Repository adapter: http depends_on: [fetch-team] condition: "{{ ne .steps.fetch-team.outputs.team nil }}" config: url: "https://api.github.com/orgs/{{ .inputs.owner }}/repos" method: POST headers: Accept: "application/vnd.github+json" Authorization: "Bearer {{ .secrets.GITHUB_TOKEN }}" body: name: "{{ .inputs.repo_name }}" description: "Repository for team {{ .inputs.team_id }}" private: true auto_init: true
- id: summary adapter: log depends_on: [create-repo] config: message: "Created repository {{ .inputs.repo_name }} for team {{ .inputs.team_id }}"
outputs: team: value: "{{ .steps.fetch-team.outputs.team }}" repo: value: "{{ .steps.create-repo.outputs.body }}"Running a mold
Section titled “Running a mold”- Open Forge in the sidebar
- Find your mold by name, category, or tags
- Click the mold card to open it
- Fill in the input form — required fields are marked
- Click the primary action button to execute
- Monitor progress in the Events tab on the run detail page
Enable Dry Run to validate inputs without actually executing the steps.
Provide an Idempotency Key to prevent duplicate executions when retrying.
- All documented Forge actions are idempotent. If a repo or file already exists, it returns the existing resource instead of failing. This makes molds safe to re-run.
- Use
depends_onin workflow steps to control execution order. Forge action steps run sequentially by default. - Keep molds focused. A mold that creates a repo with 5 files is better than one that tries to set up an entire platform.
- Test with dry run first, especially for molds that create real resources.
- Use
ui:options.placeholderto show users what valid input looks like. - Set sensible defaults so users only need to fill in what matters.