Skip to content

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.

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.yaml

Shoehorn’s crawler discovers these files automatically. Once indexed, they appear in the Forge section of the portal.

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 }}"

Shoehorn supports two step formats in the same YAML structure. The engine detects which one you are using automatically.

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 }}.

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:

ActionWhat it doesIdempotent
github.repo.createCreate a GitHub repositoryYes — returns existing repo if it already exists
github.repo.updateUpdate repository settingsYes
github.file.createCreate or update a file in a repoYes — updates if file exists (uses SHA)
github.template.applyApply a folder of templates into a repoYes — updates files if they exist
github.topics.setSet topics/tags on a repositoryYes
github.pr.createCreate a pull requestYes — returns existing PR if one is open
github.team.addAdd a GitHub team to a repositoryYes — updates permission if already added
github.collaborator.addAdd an individual collaboratorYes — updates permission if already added
catalog.entity.registerRegister an entity in the Shoehorn catalogYes — 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:

AdapterWhat it doesKey config fields
httpMake HTTP requests to any APIurl, method, headers, body, auth
catalogQuery the Shoehorn catalog (teams, entities)operation, id, filters
logWrite 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 stepsWorkflow steps
Step fieldaction:adapter:
Expression syntax${{ parameters.x }}{{ .inputs.x }}
Best forGitHub operationsMulti-system orchestration
Execution orderSequential (top to bottom)Explicit (depends_on:)
Conditional executionif: fieldcondition: 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.

The context namespace provides run and user metadata that is injected automatically — you don’t need to define these as inputs.

VariableDescription
${{ 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.

Inputs use JSON Schema. The UI renders a form automatically based on your schema.

TypeUI elementExample
stringText inputService name, description
string + enumDropdown selectGo version, environment
string + patternText input with validation^[a-z0-9-]+$
booleanCheckboxPrivate repo, enable monitoring
integer / numberNumber inputPort, replica count
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 Monitoring

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.

Provide default values so users don’t need to fill in every field:

defaults:
goVersion: "1.24"
replicas: 2
enableMonitoring: true
private: true
environment: production

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: 1

When 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.

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 }}"
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 }}"

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:options with 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 }}"
  1. Open Forge in the sidebar
  2. Find your mold by name, category, or tags
  3. Click the mold card to open it
  4. Fill in the input form — required fields are marked
  5. Click the primary action button to execute
  6. 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_on in 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.placeholder to show users what valid input looks like.
  • Set sensible defaults so users only need to fill in what matters.